diff --git a/webapp/channels/.editorconfig b/.editorconfig similarity index 72% rename from webapp/channels/.editorconfig rename to .editorconfig index b8b1577f854..0e4a261ebed 100644 --- a/webapp/channels/.editorconfig +++ b/.editorconfig @@ -8,7 +8,7 @@ insert_final_newline = true trim_trailing_whitespace = true charset = utf-8 -[*.go] +[*.{go,sql}] indent_style = tab [*.{js,jsx,json,html,ts,tsx}] @@ -21,12 +21,21 @@ indent_size = 2 [i18n/**.json] indent_size = 2 -[Makefile] +[Makefile,*.mk] indent_style = tab [*.scss] indent_style = space indent_size = 4 +[*.md] +indent_style = space +indent_size = 4 +trim_trailing_whitespace = false + [.npm-upgrade.json] insert_final_newline = false + +[*.yml,*.yaml] +indent_style = space +indent_size = 2 diff --git a/.github/workflows/e2e-tests-ci.yml b/.github/workflows/e2e-tests-ci.yml index f169995a946..34c605a8419 100644 --- a/.github/workflows/e2e-tests-ci.yml +++ b/.github/workflows/e2e-tests-ci.yml @@ -8,10 +8,6 @@ on: type: string required: false -defaults: - run: - shell: bash - jobs: update-initial-status: runs-on: ubuntu-22.04 @@ -84,25 +80,46 @@ jobs: npm run check smoketests: - runs-on: ubuntu-latest-8-cores + strategy: + matrix: + # + # Note that smoketests should be run only on ubuntu, for QA purposes. + # But it's useful to be able to run and debug the smoketests for different OSes. + # Notes: + # - For MacOS: works on developer machines, but uses too many resources to be able to run on Github Actions + # - for Windows: cannot currently run on Github Actions, since the runners do not support running linux containers, at the moment + # + #os: [ubuntu-latest-8-cores, windows-2022, macos-12-xl] + os: [ubuntu-latest-8-cores] + runs-on: "${{ matrix.os }}" needs: - cypress-check - playwright-check defaults: run: + shell: bash working-directory: e2e-tests steps: - name: ci/checkout-repo uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0 with: ref: ${{ inputs.commit_sha || github.sha }} + - name: ci/setup-macos-docker + if: runner.os == 'macos' + # https://github.com/actions/runner-images/issues/17#issuecomment-1537238473 + run: | + brew install docker docker-compose + colima start + mkdir -p ~/.docker/cli-plugins + ln -sfn /usr/local/opt/docker-compose/bin/docker-compose ~/.docker/cli-plugins/docker-compose + sudo ln -sf $HOME/.colima/default/docker.sock /var/run/docker.sock - name: ci/e2e-smoketests run: | make - name: ci/e2e-smoketests-store-results uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2 with: - name: e2e-smoketests-results + name: e2e-smoketests-results-${{ matrix.os }} path: | e2e-tests/cypress/logs/ e2e-tests/cypress/results/ diff --git a/.github/workflows/migration.yml b/.github/workflows/migration.yml new file mode 100644 index 00000000000..6d572996b22 --- /dev/null +++ b/.github/workflows/migration.yml @@ -0,0 +1,54 @@ +name: Database Migration Test +on: + workflow_call: +jobs: + test: + name: MySQL -> Postgres Migration + runs-on: ubuntu-22.04 + env: + COMPOSE_PROJECT_NAME: ghactions + BUILD_IMAGE: mattermost/mattermost-build-server:20230904_golang-1.20.7 + defaults: + run: + working-directory: server + steps: + - name: Checkout mattermost project + uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0 + - name: Run docker compose + run: | + cd build + docker-compose --ansi never run --rm start_dependencies + docker-compose --ansi never exec -T minio sh -c 'mkdir -p /data/mattermost-test'; + docker-compose --ansi never ps + - name: Generate test-data + run: | + docker run --net ${COMPOSE_PROJECT_NAME}_mm-test \ + --ulimit nofile=8096:8096 \ + --env-file=build/dotenv/migration.env \ + -v $(go env GOCACHE):/go/cache \ + -e GOCACHE=/go/cache \ + -v $PWD:/mattermost \ + -w /mattermost \ + $BUILD_IMAGE \ + make test-data + - name: Migrate the DB and compare + run: | + docker run --net ${COMPOSE_PROJECT_NAME}_mm-test \ + --ulimit nofile=8096:8096 \ + --env-file=build/dotenv/migration.env \ + -v $(go env GOCACHE):/go/cache \ + -e GOCACHE=/go/cache \ + -v $PWD:/mattermost \ + -w /mattermost \ + $BUILD_IMAGE \ + make test-migration + - name: Upload artifacts + uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2 + with: + name: Migration logs + path: server/migration.log + retention-days: 7 + - name: Stop docker compose + run: | + cd build + docker-compose --ansi never stop diff --git a/.github/workflows/mmctl-test-template.yml b/.github/workflows/mmctl-test-template.yml index 3713ffee3fb..d93d8e514e7 100644 --- a/.github/workflows/mmctl-test-template.yml +++ b/.github/workflows/mmctl-test-template.yml @@ -15,14 +15,14 @@ on: required: true type: string env: - go-version: "1.19.5" + go-version: "1.20.7" jobs: test: name: ${{ inputs.name }} runs-on: ubuntu-latest-8-cores env: COMPOSE_PROJECT_NAME: ghactions - BUILD_IMAGE: mattermost/mattermost-build-server:20230118_golang-1.19.5 + BUILD_IMAGE: mattermost/mattermost-build-server:20230904_golang-1.20.7 steps: - name: Checkout mattermost project uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0 @@ -30,10 +30,15 @@ jobs: uses: actions/setup-go@4d34df0c2316fe8122ab82dc22947d607c0c91f9 # v4.0.0 with: go-version: ${{ env.go-version }} + cache-dependency-path: server/go.sum - name: Run setup-go-work run: | cd server make setup-go-work + - name: Setup needed prepackaged plugins + run: | + cd server + make prepackaged-plugins PLUGIN_PACKAGES=mattermost-plugin-jira-v3.2.5 - name: Run docker compose run: | cd server/build @@ -43,6 +48,9 @@ jobs: docker-compose --ansi never ps - name: Run mmctl Tests run: | + if [[ ${{ github.ref_name }} == 'master' ]]; then + export MMCTL_TESTFLAGS="-timeout 30m -race" + fi docker run --net ghactions_mm-test \ --ulimit nofile=8096:8096 \ --env-file=server/build/dotenv/test.env \ @@ -52,7 +60,7 @@ jobs: -v $PWD:/mattermost \ -w /mattermost/server \ $BUILD_IMAGE \ - make test-mmctl-coverage BUILD_NUMBER=$GITHUB_HEAD_REF-$GITHUB_RUN_ID + make test-mmctl BUILD_NUMBER=$GITHUB_HEAD_REF-$GITHUB_RUN_ID - name: Stop docker compose run: | cd server/build diff --git a/.github/workflows/scorecards-analysis.yml b/.github/workflows/scorecards-analysis.yml index bfdcf117fc1..46ff7a9532b 100644 --- a/.github/workflows/scorecards-analysis.yml +++ b/.github/workflows/scorecards-analysis.yml @@ -3,45 +3,52 @@ on: # Only the default branch is supported. branch_protection_rule: schedule: - - cron: '44 6 * * *' + - cron: "44 6 * * *" # Declare default permissions as read only. permissions: read-all jobs: analysis: - name: Scorecards analysis + name: Scorecard analysis if: github.repository_owner == 'mattermost' runs-on: ubuntu-latest permissions: # Needed to upload the results to code-scanning dashboard. security-events: write - actions: read - contents: read + # Needed to publish results and get a badge (see publish_results below). + id-token: write steps: - name: "Checkout code" - uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579 # v2.4.0 + uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 # v3.1.0 with: persist-credentials: false - name: "Run analysis" - uses: ossf/scorecard-action@c1aec4ac820532bab364f02a81873c555a0ba3a1 # v1.0.4 + uses: ossf/scorecard-action@e38b1902ae4f44df626f11ba0734b14fb91f8f86 # v2.1.2 with: results_file: results.sarif results_format: sarif - # Read-only PAT token. To create it, - # follow the steps in https://github.com/ossf/scorecard-action#pat-token-creation. + # (Optional) "write" PAT token. Uncomment the `repo_token` line below if: + # - you want to enable the Branch-Protection check on a *public* repository, or + # - you are installing Scorecard on a *private* repository + # To create the PAT, follow the steps in https://github.com/ossf/scorecard-action#authentication-with-pat. repo_token: ${{ secrets.SCORECARD_READ_TOKEN }} - # Publish the results to enable scorecard badges. For more details, see - # https://github.com/ossf/scorecard-action#publishing-results. - # For private repositories, `publish_results` will automatically be set to `false`, - # regardless of the value entered here. + + # Public repositories: + # - Publish results to OpenSSF REST API for easy access by consumers + # - Allows the repository to include the Scorecard badge. + # - See https://github.com/ossf/scorecard-action#publishing-results. + # For private repositories: + # - `publish_results` will always be set to `false`, regardless + # of the value entered here. publish_results: true - # Upload the results as artifacts (optional). + # Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF + # format to the repository Actions tab. - name: "Upload artifact" - uses: actions/upload-artifact@82c141cc518b40d92cc801eee768e7aafc9c2fa2 # v2.3.1 + uses: actions/upload-artifact@3cea5372237819ed00197afe530f5a7ea3e805c8 # v3.1.0 with: name: SARIF file path: results.sarif @@ -49,6 +56,6 @@ jobs: # Upload the results to GitHub's code scanning dashboard. - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@5f532563584d71fdef14ee64d17bafb34f751ce5 # v1.0.26 + uses: github/codeql-action/upload-sarif@17573ee1cc1b9d061760f3a006fc4aac4f944fd5 # v2.2.4 with: sarif_file: results.sarif diff --git a/.github/workflows/server-ci-master.yml b/.github/workflows/server-ci-master.yml index 96f58e9fdad..a482309afc7 100644 --- a/.github/workflows/server-ci-master.yml +++ b/.github/workflows/server-ci-master.yml @@ -7,7 +7,7 @@ on: - release-* - mono-repo* env: - go-version: "1.19.5" + go-version: "1.20.7" jobs: master-ci: diff --git a/.github/workflows/server-ci-pr.yml b/.github/workflows/server-ci-pr.yml index 1028163fdfc..7a51212bb50 100644 --- a/.github/workflows/server-ci-pr.yml +++ b/.github/workflows/server-ci-pr.yml @@ -6,7 +6,7 @@ on: - "e2e-tests/**" - ".github/**" env: - go-version: "1.19.5" + go-version: "1.20.7" concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true diff --git a/.github/workflows/server-ci-template.yml b/.github/workflows/server-ci-template.yml index 3de3fd690e3..e15d565affb 100644 --- a/.github/workflows/server-ci-template.yml +++ b/.github/workflows/server-ci-template.yml @@ -6,7 +6,7 @@ on: workflow_call: env: - go-version: "1.19.5" + go-version: "1.20.7" jobs: check-mocks: @@ -297,3 +297,7 @@ jobs: name: server-build-artifact path: server/build/ retention-days: 14 + test-migration: + name: MySQL to PostgreSQL Migration + uses: ./.github/workflows/migration.yml + secrets: inherit diff --git a/.github/workflows/server-test-template.yml b/.github/workflows/server-test-template.yml index 876dfef1c1c..52c77685677 100644 --- a/.github/workflows/server-test-template.yml +++ b/.github/workflows/server-test-template.yml @@ -15,14 +15,14 @@ on: required: true type: string env: - go-version: "1.19.5" + go-version: "1.20.7" jobs: test: name: ${{ inputs.name }} runs-on: ubuntu-latest-8-cores env: COMPOSE_PROJECT_NAME: ghactions - BUILD_IMAGE: mattermost/mattermost-build-server:20230118_golang-1.19.5 + BUILD_IMAGE: mattermost/mattermost-build-server:20230904_golang-1.20.7 steps: - name: Checkout mattermost project uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0 @@ -76,12 +76,25 @@ jobs: report_paths: server/report.xml check_name: ${{ inputs.name }} (Results) require_tests: true - - name: Report retried tests via webhook - if: ${{ steps.report.outputs.retried > 0 }} + - name: Report retried tests via webhook (master || release-*) + if: ${{ steps.report.outputs.retried > 0 && (github.ref_name == 'master' || startsWith(github.ref_name, 'release-')) }} run: | curl \ --fail \ -X POST \ -H "Content-Type: application/json" \ - -d "{\"text\":\"#### ⚠️ One or more flaky tests detected ⚠️\\n* Failing job: [github.com/mattermost/mattermost:${{ inputs.name }}](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})\\n* If this is your pull request, double check your code to ensure you haven't introduced a flaky test.\\n* If this occurred on master or a release branch, reply to this message if you're willing to help.\\n* Submit a separate pull request to skip the flaky tests (e.g. [23360](https://github.com/mattermost/mattermost/pull/23360)) and file JIRA ticket (e.g. [MM-52743](https://mattermost.atlassian.net/browse/MM-52743)) for later investigation.\\n* Finally, reply to this message with a link to the created JIRA ticket.\\ncc @devs\"}" \ + -d "{\"text\":\"#### ⚠️ One or more flaky tests detected ⚠️\\n* Failing job: [github.com/mattermost/mattermost:${{ inputs.name }}](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})\\n* Ideally, this would have been caught in a pull request, but now a volunteer is required. If you're willing to help, submit a separate pull request to skip the flaky tests (e.g. [23360](https://github.com/mattermost/mattermost/pull/23360)) and file JIRA ticket (e.g. [MM-52743](https://mattermost.atlassian.net/browse/MM-52743)) for later investigation.\\n* Finally, reply to this message with a link to the created JIRA ticket.\\n\"}" \ ${{ secrets.MM_COMMUNITY_DEVELOPERS_INCOMING_WEBHOOK_FROM_GH_ACTIONS }} + - name: Report retried tests (pull request) + uses: actions/github-script@d7906e4ad0b1822421a7e6a35d5ca353c962f410 # v6.4.1 + if: ${{ steps.report.outputs.retried > 0 && !(github.ref_name == 'master' || startsWith(github.ref_name, 'release-')) }} + with: + script: | + const body = `#### ⚠️ One or more flaky tests detected ⚠️\n* Failing job: [github.com/mattermost/mattermost:${{ inputs.name }}](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})\n* Double check your code to ensure you haven't introduced a flaky test.\n* If this seems to be unrelated to your changes, submit a separate pull request to skip the flaky tests (e.g. [23360](https://github.com/mattermost/mattermost/pull/23360)) and file JIRA ticket (e.g. [MM-52743](https://mattermost.atlassian.net/browse/MM-52743)) for later investigation.` + + await github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: body + }) diff --git a/.gitignore b/.gitignore index 821153772f6..b6e49b89443 100644 --- a/.gitignore +++ b/.gitignore @@ -44,6 +44,9 @@ e2e-tests/playwright/tests/**/*-window.png e2e-tests/playwright/tests/accessibility/**/*-snapshots e2e-tests/playwright/.eslintcache +# ignore temporary added configuration for pgloader +server/tests/temp.load + # Enterprise & products imports files imports/imports.go diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 84f8176492a..b33b386d74b 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -12,7 +12,7 @@ include: variables: BUILD: "yes" - IMAGE_BUILD_SERVER: $CI_REGISTRY/mattermost/ci/images/mattermost-build-server:20230118_golang-1.19.5 + IMAGE_BUILD_SERVER: $CI_REGISTRY/mattermost/ci/images/mattermost-build-server:20230904_golang-1.20.7 IMAGE_BUILD_DOCKER: $CI_REGISTRY/mattermost/ci/devops-images/mattermost-build-docker:23.0.1-0 IMAGE_DIND: $CI_REGISTRY/mattermost/ci/devops-images/docker-dind:23.0.1-0 diff --git a/.nvmrc b/.nvmrc index 3027af39c1b..aacb5181047 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -16.10.0 +18.17 diff --git a/api/Makefile b/api/Makefile index 0c78f2973d7..387da3e31a0 100644 --- a/api/Makefile +++ b/api/Makefile @@ -41,7 +41,6 @@ build-v4: node_modules playbooks @cat $(V4_SRC)/schemes.yaml >> $(V4_YAML) @cat $(V4_SRC)/service_terms.yaml >> $(V4_YAML) @cat $(V4_SRC)/sharedchannels.yaml >> $(V4_YAML) - @cat $(V4_SRC)/opengraph.yaml >> $(V4_YAML) @cat $(V4_SRC)/reactions.yaml >> $(V4_YAML) @cat $(V4_SRC)/actions.yaml >> $(V4_YAML) @cat $(V4_SRC)/bots.yaml >> $(V4_YAML) diff --git a/api/v4/html/static/favicon.ico b/api/v4/html/static/favicon.ico index 178f1c56729..884e9a39254 100644 Binary files a/api/v4/html/static/favicon.ico and b/api/v4/html/static/favicon.ico differ diff --git a/api/v4/source/opengraph.yaml b/api/v4/source/opengraph.yaml deleted file mode 100644 index 3cde79d4be4..00000000000 --- a/api/v4/source/opengraph.yaml +++ /dev/null @@ -1,38 +0,0 @@ - /api/v4/opengraph: - post: - tags: - - OpenGraph - summary: Get open graph metadata for url - description: > - Get Open Graph Metadata for a specif URL. Use the Open Graph protocol to - get some generic metadata about a URL. Used for creating link previews. - - - __Minimum server version__: 3.10 - - - ##### Permissions - - No permission required but must be logged in. - operationId: OpenGraph - requestBody: - content: - application/json: - schema: - type: object - required: - - url - properties: - url: - type: string - description: The URL to get Open Graph Metadata. - required: true - responses: - "200": - description: Open Graph retrieval successful - content: - application/json: - schema: - $ref: "#/components/schemas/OpenGraph" - "501": - $ref: "#/components/responses/NotImplemented" diff --git a/e2e-tests/.ci/.e2erc b/e2e-tests/.ci/.e2erc index 87961fc407a..e08232620b9 100644 --- a/e2e-tests/.ci/.e2erc +++ b/e2e-tests/.ci/.e2erc @@ -3,10 +3,23 @@ export MME2E_DC_SERVER="docker compose -p mmserver -f $(readlink -f ../../server/build/gitlab-dc.postgres.yml) -f $(readlink -f ./server.override.yml)" export MME2E_DC_DASHBOARD="docker compose -p mmdashboard -f $(readlink -f ./dashboard/docker/docker-compose.yml) -f $(readlink -f ./dashboard.override.yml)" export MME2E_UID=$(id -u) +export MME2E_OSTYPE=$(docker version -f '{{ .Client.Os }}') +export MME2E_ARCHTYPE=$(docker version -f '{{ .Client.Arch }}') +export NODE_VERSION_REQUIRED=$(cat ../../.nvmrc) -# Default the optional variables that are used in the docker-compose file +# Default values for optional variables export SERVER_IMAGE_DEFAULT="mattermostdevelopment/mm-ee-test:$(git rev-parse --short=7 HEAD)" +export BROWSER_DEFAULT="chrome" +case $MME2E_OSTYPE in + # OS specific defaults overrides + darwin ) + BROWSER_DEFAULT="electron" ;; + * ) +esac + +# Populate the optional variables that are used in the docker-compose file export SERVER_IMAGE=${SERVER_IMAGE:-$SERVER_IMAGE_DEFAULT} +export BROWSER=${BROWSER:-$BROWSER_DEFAULT} # Function definitions mme2e_log () { echo "[$(date +%Y-%m-%dT%H:%M:%S%Z)]" "$@"; } @@ -69,7 +82,14 @@ mme2e_legacy_setup () { # These functions are needed until every pipeline depending on the `server/build/gitlab-dc.*.yml` files is adapted to not use external docker networking anymore # After that is fixed, this function and the external network in the docker-compose files may be removed export COMPOSE_PROJECT_NAME=mmserver_legacy - docker network inspect ${COMPOSE_PROJECT_NAME} >/dev/null 2>&1 || docker network create ${COMPOSE_PROJECT_NAME} + case $MME2E_OSTYPE in + windows ) + # https://learn.microsoft.com/en-us/virtualization/windowscontainers/container-networking/network-drivers-topologies + DOCKER_NETWORK_DRIVER="nat" ;; + * ) + DOCKER_NETWORK_DRIVER="bridge" ;; + esac + docker network inspect ${COMPOSE_PROJECT_NAME} >/dev/null 2>&1 || docker network create --driver=${DOCKER_NETWORK_DRIVER} ${COMPOSE_PROJECT_NAME} } # Utility alias, for interactive shell usage. Can be reversed with 'unalias docker-compose' in your shell diff --git a/e2e-tests/.ci/dashboard.override.yml b/e2e-tests/.ci/dashboard.override.yml index 7e20368f634..6ebd3526fa8 100644 --- a/e2e-tests/.ci/dashboard.override.yml +++ b/e2e-tests/.ci/dashboard.override.yml @@ -1,24 +1,23 @@ --- -version: '3.1' +version: "3.1" services: - dashboard: - image: mattermostdevelopment/mirrored-node:16.17.0 - environment: - PG_URI: postgres://mmuser:mostest@db:5432/automation_dashboard_db - JWT_SECRET: s8gGBA3ujKRohSw1L8HLOY7Jjnu2ZYv8 # Generated with e.g. `dd if=/dev/urandom count=24 bs=1 2>/dev/null | base64 -w0` - JWT_USER: cypress-test - JWT_ROLE: integration - JWT_ALG: HS256 - JWT_EXPIRES_IN: 365d - ALLOWED_USER: cypress-test - ALLOWED_ROLE: integration - user: "${MME2E_UID}" - volumes: - - "../:/app" - - "../../dashboard.entrypoint.sh:/usr/local/bin/dashboard.entrypoint.sh:ro" - - "../../.env.dashboard:/var/local/.env.dashboard:rw" - working_dir: /app - entrypoint: /usr/local/bin/dashboard.entrypoint.sh - ports: - - 4000:4000 + dashboard: + image: mattermostdevelopment/mirrored-node:18.17 + environment: + PG_URI: postgres://mmuser:mostest@db:5432/automation_dashboard_db + JWT_SECRET: s8gGBA3ujKRohSw1L8HLOY7Jjnu2ZYv8 # Generated with e.g. `dd if=/dev/urandom count=24 bs=1 2>/dev/null | base64 -w0` + JWT_USER: cypress-test + JWT_ROLE: integration + JWT_ALG: HS256 + JWT_EXPIRES_IN: 365d + ALLOWED_USER: cypress-test + ALLOWED_ROLE: integration + volumes: + - "../:/app" + - "../../dashboard.entrypoint.sh:/usr/local/bin/dashboard.entrypoint.sh:ro" + - "../../.env.dashboard:/var/local/.env.dashboard:rw" + working_dir: /app + entrypoint: /usr/local/bin/dashboard.entrypoint.sh + ports: + - 4000:4000 diff --git a/e2e-tests/.ci/dashboard.start.sh b/e2e-tests/.ci/dashboard.start.sh index b17f927280b..d21092dc99f 100755 --- a/e2e-tests/.ci/dashboard.start.sh +++ b/e2e-tests/.ci/dashboard.start.sh @@ -18,13 +18,18 @@ mme2e_log "Starting the dashboard" ${MME2E_DC_DASHBOARD} up -d db dashboard mme2e_log "Generating the dashboard's local URL" -MME2E_DC_DASHBOARD_NETWORK=$(${MME2E_DC_DASHBOARD} ps -q dashboard | xargs -l docker inspect | jq -r '.[0].NetworkSettings.Networks | (keys|.[0])') -MME2E_DC_DASHBOARD_GATEWAY=$(docker network inspect $MME2E_DC_DASHBOARD_NETWORK | jq -r '.[0].IPAM.Config[0].Gateway') -AUTOMATION_DASHBOARD_URL="http://${MME2E_DC_DASHBOARD_GATEWAY}:4000/api" +case $MME2E_OSTYPE in + darwin ) + AUTOMATION_DASHBOARD_IP=$(ifconfig $(route get 1.1.1.1 | grep interface | awk '{print $2}') | grep -w inet | awk '{print $2}') ;; + * ) + MME2E_DC_DASHBOARD_NETWORK=$(docker inspect $(${MME2E_DC_DASHBOARD} ps -q dashboard) | jq -r '.[0].NetworkSettings.Networks | (keys|.[0])') + AUTOMATION_DASHBOARD_IP=$(docker network inspect $MME2E_DC_DASHBOARD_NETWORK | jq -r '.[0].IPAM.Config[0].Gateway') +esac +AUTOMATION_DASHBOARD_URL="http://${AUTOMATION_DASHBOARD_IP}:4000/api" mme2e_log "Generating a signed JWT token for accessing the dashboard" -${MME2E_DC_DASHBOARD} exec -T -u $MME2E_UID dashboard npm i -AUTOMATION_DASHBOARD_TOKEN=$(${MME2E_DC_DASHBOARD} exec -T -u $MME2E_UID dashboard node script/sign.js | awk '{ print $2; }') # The token secret is specified in the dashboard.override.yml file +${MME2E_DC_DASHBOARD} exec -T dashboard npm i +AUTOMATION_DASHBOARD_TOKEN=$(${MME2E_DC_DASHBOARD} exec -T dashboard node script/sign.js | awk '{ print $2; }') # The token secret is specified in the dashboard.override.yml file mme2e_log "Generating the .env.dashboard file, to point Cypress to the dashboard URL" mme2e_generate_envfile_from_var_names >.env.dashboard <.env.cypress <=0.10.0" + } + }, "node_modules/@ampproject/remapping": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.0.tgz", @@ -153,20 +158,20 @@ } }, "node_modules/@babel/eslint-parser": { - "version": "7.21.8", - "resolved": "https://registry.npmjs.org/@babel/eslint-parser/-/eslint-parser-7.21.8.tgz", - "integrity": "sha512-HLhI+2q+BP3sf78mFUZNCGc10KEmoUqtUT1OCdMZsN+qr4qFeLUod62/zAnF3jNQstwyasDkZnVXwfK2Bml7MQ==", + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/eslint-parser/-/eslint-parser-7.22.15.tgz", + "integrity": "sha512-yc8OOBIQk1EcRrpizuARSQS0TWAcOMpEJ1aafhNznaeYkeL+OhqnDObGFylB8ka8VFF/sZc+S4RzHyO+3LjQxg==", "dev": true, "dependencies": { "@nicolo-ribaudo/eslint-scope-5-internals": "5.1.1-v1", "eslint-visitor-keys": "^2.1.0", - "semver": "^6.3.0" + "semver": "^6.3.1" }, "engines": { "node": "^10.13.0 || ^12.13.0 || >=14.0.0" }, "peerDependencies": { - "@babel/core": ">=7.11.0", + "@babel/core": "^7.11.0", "eslint": "^7.5.0 || ^8.0.0" } }, @@ -179,10 +184,19 @@ "node": ">=10" } }, + "node_modules/@babel/eslint-parser/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, "node_modules/@babel/eslint-plugin": { - "version": "7.19.1", - "resolved": "https://registry.npmjs.org/@babel/eslint-plugin/-/eslint-plugin-7.19.1.tgz", - "integrity": "sha512-ElGPkQPapKMa3zVqXHkZYzuL7I5LbRw9UWBUArgWsdWDDb9XcACqOpBib5tRPA9XvbVZYrFUkoQPbiJ4BFvu4w==", + "version": "7.22.10", + "resolved": "https://registry.npmjs.org/@babel/eslint-plugin/-/eslint-plugin-7.22.10.tgz", + "integrity": "sha512-SRZcvo3fnO5h79B9DZSV6LG2vHH7OWsSNp1huFLHsXKyytRG413byQk9zxW1VcPOhnzfx2VIUz+8aGbiE7fOkA==", "dev": true, "dependencies": { "eslint-rule-composer": "^0.3.0" @@ -191,8 +205,8 @@ "node": "^10.13.0 || ^12.13.0 || >=14.0.0" }, "peerDependencies": { - "@babel/eslint-parser": ">=7.11.0", - "eslint": ">=7.5.0" + "@babel/eslint-parser": "^7.11.0", + "eslint": "^7.5.0 || ^8.0.0" } }, "node_modules/@babel/generator": { @@ -226,33 +240,6 @@ "node": ">=6.0.0" } }, - "node_modules/@babel/helper-annotate-as-pure": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.18.6.tgz", - "integrity": "sha512-duORpUiYrEpzKIop6iNbjnwKLAKnJ47csTyRACyEmWj0QdUrm5aqNJGHSSEQSUAvNW0ojX0dOmK9dZduvkfeXA==", - "dev": true, - "peer": true, - "dependencies": { - "@babel/types": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-builder-binary-assignment-operator-visitor": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.18.6.tgz", - "integrity": "sha512-KT10c1oWEpmrIRYnthbzHgoOf6B+Xd6a5yhdbNtdhtG7aO1or5HViuf1TQR36xY/QprXA5nvxO6nAjhJ4y38jw==", - "dev": true, - "peer": true, - "dependencies": { - "@babel/helper-explode-assignable-expression": "^7.18.6", - "@babel/types": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@babel/helper-compilation-targets": { "version": "7.20.7", "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.20.7.tgz", @@ -290,63 +277,6 @@ "dev": true, "peer": true }, - "node_modules/@babel/helper-create-class-features-plugin": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.18.6.tgz", - "integrity": "sha512-YfDzdnoxHGV8CzqHGyCbFvXg5QESPFkXlHtvdCkesLjjVMT2Adxe4FGUR5ChIb3DxSaXO12iIOCWoXdsUVwnqw==", - "dev": true, - "peer": true, - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.18.6", - "@babel/helper-environment-visitor": "^7.18.6", - "@babel/helper-function-name": "^7.18.6", - "@babel/helper-member-expression-to-functions": "^7.18.6", - "@babel/helper-optimise-call-expression": "^7.18.6", - "@babel/helper-replace-supers": "^7.18.6", - "@babel/helper-split-export-declaration": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-create-regexp-features-plugin": { - "version": "7.21.0", - "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.21.0.tgz", - "integrity": "sha512-N+LaFW/auRSWdx7SHD/HiARwXQju1vXTW4fKr4u5SgBUTm51OKEjKgj+cs00ggW3kEvNqwErnlwuq7Y3xBe4eg==", - "dev": true, - "peer": true, - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.18.6", - "regexpu-core": "^5.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-define-polyfill-provider": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.3.3.tgz", - "integrity": "sha512-z5aQKU4IzbqCC1XH0nAqfsFLMVSo22SBKUc0BxGrLkolTdPTructy0ToNnlO2zA4j9Q/7pjMZf0DSY+DSTYzww==", - "dev": true, - "peer": true, - "dependencies": { - "@babel/helper-compilation-targets": "^7.17.7", - "@babel/helper-plugin-utils": "^7.16.7", - "debug": "^4.1.1", - "lodash.debounce": "^4.0.8", - "resolve": "^1.14.2", - "semver": "^6.1.2" - }, - "peerDependencies": { - "@babel/core": "^7.4.0-0" - } - }, "node_modules/@babel/helper-environment-visitor": { "version": "7.18.9", "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.9.tgz", @@ -357,19 +287,6 @@ "node": ">=6.9.0" } }, - "node_modules/@babel/helper-explode-assignable-expression": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.18.6.tgz", - "integrity": "sha512-eyAYAsQmB80jNfg4baAtLeWAQHfHFiR483rzFK+BhETlGZaQC9bsfrugfXDCbRHLQbIA7U5NxhhOxN7p/dWIcg==", - "dev": true, - "peer": true, - "dependencies": { - "@babel/types": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@babel/helper-function-name": { "version": "7.21.0", "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.21.0.tgz", @@ -397,19 +314,6 @@ "node": ">=6.9.0" } }, - "node_modules/@babel/helper-member-expression-to-functions": { - "version": "7.21.0", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.21.0.tgz", - "integrity": "sha512-Muu8cdZwNN6mRRNG6lAYErJ5X3bRevgYR2O8wN0yn7jJSnGDu6eG59RfT29JHxGUovyfrh6Pj0XzmR7drNVL3Q==", - "dev": true, - "peer": true, - "dependencies": { - "@babel/types": "^7.21.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@babel/helper-module-imports": { "version": "7.18.6", "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.18.6.tgz", @@ -443,66 +347,6 @@ "node": ">=6.9.0" } }, - "node_modules/@babel/helper-optimise-call-expression": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.18.6.tgz", - "integrity": "sha512-HP59oD9/fEHQkdcbgFCnbmgH5vIQTJbxh2yf+CdM89/glUNnuzr87Q8GIjGEnOktTROemO0Pe0iPAYbqZuOUiA==", - "dev": true, - "peer": true, - "dependencies": { - "@babel/types": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-plugin-utils": { - "version": "7.20.2", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.20.2.tgz", - "integrity": "sha512-8RvlJG2mj4huQ4pZ+rU9lqKi9ZKiRmuvGuM2HlWmkmgOhbs6zEAw6IEiJ5cQqGbDzGZOhwuOQNtZMi/ENLjZoQ==", - "dev": true, - "peer": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-remap-async-to-generator": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.18.9.tgz", - "integrity": "sha512-dI7q50YKd8BAv3VEfgg7PS7yD3Rtbi2J1XMXaalXO0W0164hYLnh8zpjRS0mte9MfVp/tltvr/cfdXPvJr1opA==", - "dev": true, - "peer": true, - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.18.6", - "@babel/helper-environment-visitor": "^7.18.9", - "@babel/helper-wrap-function": "^7.18.9", - "@babel/types": "^7.18.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-replace-supers": { - "version": "7.20.7", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.20.7.tgz", - "integrity": "sha512-vujDMtB6LVfNW13jhlCrp48QNslK6JXi7lQG736HVbHz/mbf4Dc7tIRh1Xf5C0rF7BP8iiSxGMCmY6Ci1ven3A==", - "dev": true, - "peer": true, - "dependencies": { - "@babel/helper-environment-visitor": "^7.18.9", - "@babel/helper-member-expression-to-functions": "^7.20.7", - "@babel/helper-optimise-call-expression": "^7.18.6", - "@babel/template": "^7.20.7", - "@babel/traverse": "^7.20.7", - "@babel/types": "^7.20.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@babel/helper-simple-access": { "version": "7.20.2", "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.20.2.tgz", @@ -516,19 +360,6 @@ "node": ">=6.9.0" } }, - "node_modules/@babel/helper-skip-transparent-expression-wrappers": { - "version": "7.20.0", - "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.20.0.tgz", - "integrity": "sha512-5y1JYeNKfvnT8sZcK9DVRtpTbGiomYIHviSP3OQWmDPU3DeH4a1ZlT/N2lyQ5P8egjcRaT/Y9aNqUxK0WsnIIg==", - "dev": true, - "peer": true, - "dependencies": { - "@babel/types": "^7.20.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@babel/helper-split-export-declaration": { "version": "7.18.6", "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.18.6.tgz", @@ -571,22 +402,6 @@ "node": ">=6.9.0" } }, - "node_modules/@babel/helper-wrap-function": { - "version": "7.20.5", - "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.20.5.tgz", - "integrity": "sha512-bYMxIWK5mh+TgXGVqAtnu5Yn1un+v8DDZtqyzKRLUzrh70Eal2O3aZ7aPYiMADO4uKlkzOiRiZ6GX5q3qxvW9Q==", - "dev": true, - "peer": true, - "dependencies": { - "@babel/helper-function-name": "^7.19.0", - "@babel/template": "^7.18.10", - "@babel/traverse": "^7.20.5", - "@babel/types": "^7.20.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@babel/helpers": { "version": "7.21.0", "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.21.0.tgz", @@ -691,1452 +506,6 @@ "node": ">=6.0.0" } }, - "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.18.6.tgz", - "integrity": "sha512-Dgxsyg54Fx1d4Nge8UnvTrED63vrwOdPmyvPzlNN/boaliRP54pm3pGzZD1SJUwrBA+Cs/xdG8kXX6Mn/RfISQ==", - "dev": true, - "peer": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { - "version": "7.20.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.20.7.tgz", - "integrity": "sha512-sbr9+wNE5aXMBBFBICk01tt7sBf2Oc9ikRFEcem/ZORup9IMUdNhW7/wVLEbbtlWOsEubJet46mHAL2C8+2jKQ==", - "dev": true, - "peer": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.20.2", - "@babel/helper-skip-transparent-expression-wrappers": "^7.20.0", - "@babel/plugin-proposal-optional-chaining": "^7.20.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.13.0" - } - }, - "node_modules/@babel/plugin-proposal-async-generator-functions": { - "version": "7.20.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.20.7.tgz", - "integrity": "sha512-xMbiLsn/8RK7Wq7VeVytytS2L6qE69bXPB10YCmMdDZbKF4okCqY74pI/jJQ/8U0b/F6NrT2+14b8/P9/3AMGA==", - "dev": true, - "peer": true, - "dependencies": { - "@babel/helper-environment-visitor": "^7.18.9", - "@babel/helper-plugin-utils": "^7.20.2", - "@babel/helper-remap-async-to-generator": "^7.18.9", - "@babel/plugin-syntax-async-generators": "^7.8.4" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-proposal-class-properties": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.18.6.tgz", - "integrity": "sha512-cumfXOF0+nzZrrN8Rf0t7M+tF6sZc7vhQwYQck9q1/5w2OExlD+b4v4RpMJFaV1Z7WcDRgO6FqvxqxGlwo+RHQ==", - "dev": true, - "peer": true, - "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-proposal-class-static-block": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-static-block/-/plugin-proposal-class-static-block-7.18.6.tgz", - "integrity": "sha512-+I3oIiNxrCpup3Gi8n5IGMwj0gOCAjcJUSQEcotNnCCPMEnixawOQ+KeJPlgfjzx+FKQ1QSyZOWe7wmoJp7vhw==", - "dev": true, - "peer": true, - "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6", - "@babel/plugin-syntax-class-static-block": "^7.14.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.12.0" - } - }, - "node_modules/@babel/plugin-proposal-dynamic-import": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.18.6.tgz", - "integrity": "sha512-1auuwmK+Rz13SJj36R+jqFPMJWyKEDd7lLSdOj4oJK0UTgGueSAtkrCvz9ewmgyU/P941Rv2fQwZJN8s6QruXw==", - "dev": true, - "peer": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6", - "@babel/plugin-syntax-dynamic-import": "^7.8.3" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-proposal-export-default-from": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-export-default-from/-/plugin-proposal-export-default-from-7.18.6.tgz", - "integrity": "sha512-oTvzWB16T9cB4j5kX8c8DuUHo/4QtR2P9vnUNKed9xqFP8Jos/IRniz1FiIryn6luDYoltDJSYF7RCpbm2doMg==", - "dev": true, - "peer": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6", - "@babel/plugin-syntax-export-default-from": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-proposal-export-namespace-from": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-export-namespace-from/-/plugin-proposal-export-namespace-from-7.18.9.tgz", - "integrity": "sha512-k1NtHyOMvlDDFeb9G5PhUXuGj8m/wiwojgQVEhJ/fsVsMCpLyOP4h0uGEjYJKrRI+EVPlb5Jk+Gt9P97lOGwtA==", - "dev": true, - "peer": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.9", - "@babel/plugin-syntax-export-namespace-from": "^7.8.3" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-proposal-json-strings": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.18.6.tgz", - "integrity": "sha512-lr1peyn9kOdbYc0xr0OdHTZ5FMqS6Di+H0Fz2I/JwMzGmzJETNeOFq2pBySw6X/KFL5EWDjlJuMsUGRFb8fQgQ==", - "dev": true, - "peer": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6", - "@babel/plugin-syntax-json-strings": "^7.8.3" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-proposal-logical-assignment-operators": { - "version": "7.20.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-logical-assignment-operators/-/plugin-proposal-logical-assignment-operators-7.20.7.tgz", - "integrity": "sha512-y7C7cZgpMIjWlKE5T7eJwp+tnRYM89HmRvWM5EQuB5BoHEONjmQ8lSNmBUwOyy/GFRsohJED51YBF79hE1djug==", - "dev": true, - "peer": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.20.2", - "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-proposal-nullish-coalescing-operator": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.18.6.tgz", - "integrity": "sha512-wQxQzxYeJqHcfppzBDnm1yAY0jSRkUXR2z8RePZYrKwMKgMlE8+Z6LUno+bd6LvbGh8Gltvy74+9pIYkr+XkKA==", - "dev": true, - "peer": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6", - "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-proposal-numeric-separator": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.18.6.tgz", - "integrity": "sha512-ozlZFogPqoLm8WBr5Z8UckIoE4YQ5KESVcNudyXOR8uqIkliTEgJ3RoketfG6pmzLdeZF0H/wjE9/cCEitBl7Q==", - "dev": true, - "peer": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6", - "@babel/plugin-syntax-numeric-separator": "^7.10.4" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-proposal-object-rest-spread": { - "version": "7.20.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.20.7.tgz", - "integrity": "sha512-d2S98yCiLxDVmBmE8UjGcfPvNEUbA1U5q5WxaWFUGRzJSVAZqm5W6MbPct0jxnegUZ0niLeNX+IOzEs7wYg9Dg==", - "dev": true, - "peer": true, - "dependencies": { - "@babel/compat-data": "^7.20.5", - "@babel/helper-compilation-targets": "^7.20.7", - "@babel/helper-plugin-utils": "^7.20.2", - "@babel/plugin-syntax-object-rest-spread": "^7.8.3", - "@babel/plugin-transform-parameters": "^7.20.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-proposal-optional-catch-binding": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.18.6.tgz", - "integrity": "sha512-Q40HEhs9DJQyaZfUjjn6vE8Cv4GmMHCYuMGIWUnlxH6400VGxOuwWsPt4FxXxJkC/5eOzgn0z21M9gMT4MOhbw==", - "dev": true, - "peer": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6", - "@babel/plugin-syntax-optional-catch-binding": "^7.8.3" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-proposal-optional-chaining": { - "version": "7.21.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.21.0.tgz", - "integrity": "sha512-p4zeefM72gpmEe2fkUr/OnOXpWEf8nAgk7ZYVqqfFiyIG7oFfVZcCrU64hWn5xp4tQ9LkV4bTIa5rD0KANpKNA==", - "dev": true, - "peer": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.20.2", - "@babel/helper-skip-transparent-expression-wrappers": "^7.20.0", - "@babel/plugin-syntax-optional-chaining": "^7.8.3" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-proposal-private-methods": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-methods/-/plugin-proposal-private-methods-7.18.6.tgz", - "integrity": "sha512-nutsvktDItsNn4rpGItSNV2sz1XwS+nfU0Rg8aCx3W3NOKVzdMjJRu0O5OkgDp3ZGICSTbgRpxZoWsxoKRvbeA==", - "dev": true, - "peer": true, - "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-proposal-private-property-in-object": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.18.6.tgz", - "integrity": "sha512-9Rysx7FOctvT5ouj5JODjAFAkgGoudQuLPamZb0v1TGLpapdNaftzifU8NTWQm0IRjqoYypdrSmyWgkocDQ8Dw==", - "dev": true, - "peer": true, - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.18.6", - "@babel/helper-create-class-features-plugin": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6", - "@babel/plugin-syntax-private-property-in-object": "^7.14.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-proposal-unicode-property-regex": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.18.6.tgz", - "integrity": "sha512-2BShG/d5yoZyXZfVePH91urL5wTG6ASZU9M4o03lKK8u8UW1y08OMttBSOADTcJrnPMpvDXRG3G8fyLh4ovs8w==", - "dev": true, - "peer": true, - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6" - }, - "engines": { - "node": ">=4" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-async-generators": { - "version": "7.8.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", - "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", - "dev": true, - "peer": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-class-properties": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", - "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", - "dev": true, - "peer": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.12.13" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-class-static-block": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", - "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", - "dev": true, - "peer": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.14.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-dynamic-import": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz", - "integrity": "sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==", - "dev": true, - "peer": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-export-default-from": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-export-default-from/-/plugin-syntax-export-default-from-7.18.6.tgz", - "integrity": "sha512-Kr//z3ujSVNx6E9z9ih5xXXMqK07VVTuqPmqGe6Mss/zW5XPeLZeSDZoP9ab/hT4wPKqAgjl2PnhPrcpk8Seew==", - "dev": true, - "peer": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-export-namespace-from": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.8.3.tgz", - "integrity": "sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q==", - "dev": true, - "peer": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.3" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-flow": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-flow/-/plugin-syntax-flow-7.18.6.tgz", - "integrity": "sha512-LUbR+KNTBWCUAqRG9ex5Gnzu2IOkt8jRJbHHXFT9q+L9zm7M/QQbEqXyw1n1pohYvOyWC8CjeyjrSaIwiYjK7A==", - "dev": true, - "peer": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-import-assertions": { - "version": "7.20.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.20.0.tgz", - "integrity": "sha512-IUh1vakzNoWalR8ch/areW7qFopR2AEw03JlG7BbrDqmQ4X3q9uuipQwSGrUn7oGiemKjtSLDhNtQHzMHr1JdQ==", - "dev": true, - "peer": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.19.0" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-json-strings": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", - "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", - "dev": true, - "peer": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-jsx": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.18.6.tgz", - "integrity": "sha512-6mmljtAedFGTWu2p/8WIORGwy+61PLgOMPOdazc7YoJ9ZCWUyFy3A6CpPkRKLKD1ToAesxX8KGEViAiLo9N+7Q==", - "dev": true, - "peer": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-logical-assignment-operators": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", - "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", - "dev": true, - "peer": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.10.4" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", - "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", - "dev": true, - "peer": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-numeric-separator": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", - "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", - "dev": true, - "peer": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.10.4" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-object-rest-spread": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", - "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", - "dev": true, - "peer": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-optional-catch-binding": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", - "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", - "dev": true, - "peer": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-optional-chaining": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", - "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", - "dev": true, - "peer": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-private-property-in-object": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", - "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", - "dev": true, - "peer": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.14.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-top-level-await": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", - "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", - "dev": true, - "peer": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.14.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-typescript": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.18.6.tgz", - "integrity": "sha512-mAWAuq4rvOepWCBid55JuRNvpTNf2UGVgoz4JV0fXEKolsVZDzsa4NqCef758WZJj/GDu0gVGItjKFiClTAmZA==", - "dev": true, - "peer": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-arrow-functions": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.18.6.tgz", - "integrity": "sha512-9S9X9RUefzrsHZmKMbDXxweEH+YlE8JJEuat9FdvW9Qh1cw7W64jELCtWNkPBPX5En45uy28KGvA/AySqUh8CQ==", - "dev": true, - "peer": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-async-to-generator": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.18.6.tgz", - "integrity": "sha512-ARE5wZLKnTgPW7/1ftQmSi1CmkqqHo2DNmtztFhvgtOWSDfq0Cq9/9L+KnZNYSNrydBekhW3rwShduf59RoXag==", - "dev": true, - "peer": true, - "dependencies": { - "@babel/helper-module-imports": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6", - "@babel/helper-remap-async-to-generator": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-block-scoped-functions": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.18.6.tgz", - "integrity": "sha512-ExUcOqpPWnliRcPqves5HJcJOvHvIIWfuS4sroBUenPuMdmW+SMHDakmtS7qOo13sVppmUijqeTv7qqGsvURpQ==", - "dev": true, - "peer": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-block-scoping": { - "version": "7.21.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.21.0.tgz", - "integrity": "sha512-Mdrbunoh9SxwFZapeHVrwFmri16+oYotcZysSzhNIVDwIAb1UV+kvnxULSYq9J3/q5MDG+4X6w8QVgD1zhBXNQ==", - "dev": true, - "peer": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.20.2" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-classes": { - "version": "7.21.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.21.0.tgz", - "integrity": "sha512-RZhbYTCEUAe6ntPehC4hlslPWosNHDox+vAs4On/mCLRLfoDVHf6hVEd7kuxr1RnHwJmxFfUM3cZiZRmPxJPXQ==", - "dev": true, - "peer": true, - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.18.6", - "@babel/helper-compilation-targets": "^7.20.7", - "@babel/helper-environment-visitor": "^7.18.9", - "@babel/helper-function-name": "^7.21.0", - "@babel/helper-optimise-call-expression": "^7.18.6", - "@babel/helper-plugin-utils": "^7.20.2", - "@babel/helper-replace-supers": "^7.20.7", - "@babel/helper-split-export-declaration": "^7.18.6", - "globals": "^11.1.0" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-computed-properties": { - "version": "7.20.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.20.7.tgz", - "integrity": "sha512-Lz7MvBK6DTjElHAmfu6bfANzKcxpyNPeYBGEafyA6E5HtRpjpZwU+u7Qrgz/2OR0z+5TvKYbPdphfSaAcZBrYQ==", - "dev": true, - "peer": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.20.2", - "@babel/template": "^7.20.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-destructuring": { - "version": "7.21.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.21.3.tgz", - "integrity": "sha512-bp6hwMFzuiE4HqYEyoGJ/V2LeIWn+hLVKc4pnj++E5XQptwhtcGmSayM029d/j2X1bPKGTlsyPwAubuU22KhMA==", - "dev": true, - "peer": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.20.2" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-dotall-regex": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.18.6.tgz", - "integrity": "sha512-6S3jpun1eEbAxq7TdjLotAsl4WpQI9DxfkycRcKrjhQYzU87qpXdknpBg/e+TdcMehqGnLFi7tnFUBR02Vq6wg==", - "dev": true, - "peer": true, - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-duplicate-keys": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.18.9.tgz", - "integrity": "sha512-d2bmXCtZXYc59/0SanQKbiWINadaJXqtvIQIzd4+hNwkWBgyCd5F/2t1kXoUdvPMrxzPvhK6EMQRROxsue+mfw==", - "dev": true, - "peer": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-exponentiation-operator": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.18.6.tgz", - "integrity": "sha512-wzEtc0+2c88FVR34aQmiz56dxEkxr2g8DQb/KfaFa1JYXOFVsbhvAonFN6PwVWj++fKmku8NP80plJ5Et4wqHw==", - "dev": true, - "peer": true, - "dependencies": { - "@babel/helper-builder-binary-assignment-operator-visitor": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-flow-strip-types": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-flow-strip-types/-/plugin-transform-flow-strip-types-7.18.6.tgz", - "integrity": "sha512-wE0xtA7csz+hw4fKPwxmu5jnzAsXPIO57XnRwzXP3T19jWh1BODnPGoG9xKYwvAwusP7iUktHayRFbMPGtODaQ==", - "dev": true, - "peer": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6", - "@babel/plugin-syntax-flow": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-for-of": { - "version": "7.18.8", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.18.8.tgz", - "integrity": "sha512-yEfTRnjuskWYo0k1mHUqrVWaZwrdq8AYbfrpqULOJOaucGSp4mNMVps+YtA8byoevxS/urwU75vyhQIxcCgiBQ==", - "dev": true, - "peer": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-function-name": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.18.9.tgz", - "integrity": "sha512-WvIBoRPaJQ5yVHzcnJFor7oS5Ls0PYixlTYE63lCj2RtdQEl15M68FXQlxnG6wdraJIXRdR7KI+hQ7q/9QjrCQ==", - "dev": true, - "peer": true, - "dependencies": { - "@babel/helper-compilation-targets": "^7.18.9", - "@babel/helper-function-name": "^7.18.9", - "@babel/helper-plugin-utils": "^7.18.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-literals": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.18.9.tgz", - "integrity": "sha512-IFQDSRoTPnrAIrI5zoZv73IFeZu2dhu6irxQjY9rNjTT53VmKg9fenjvoiOWOkJ6mm4jKVPtdMzBY98Fp4Z4cg==", - "dev": true, - "peer": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-member-expression-literals": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.18.6.tgz", - "integrity": "sha512-qSF1ihLGO3q+/g48k85tUjD033C29TNTVB2paCwZPVmOsjn9pClvYYrM2VeJpBY2bcNkuny0YUyTNRyRxJ54KA==", - "dev": true, - "peer": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-modules-amd": { - "version": "7.20.11", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.20.11.tgz", - "integrity": "sha512-NuzCt5IIYOW0O30UvqktzHYR2ud5bOWbY0yaxWZ6G+aFzOMJvrs5YHNikrbdaT15+KNO31nPOy5Fim3ku6Zb5g==", - "dev": true, - "peer": true, - "dependencies": { - "@babel/helper-module-transforms": "^7.20.11", - "@babel/helper-plugin-utils": "^7.20.2" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-modules-commonjs": { - "version": "7.21.2", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.21.2.tgz", - "integrity": "sha512-Cln+Yy04Gxua7iPdj6nOV96smLGjpElir5YwzF0LBPKoPlLDNJePNlrGGaybAJkd0zKRnOVXOgizSqPYMNYkzA==", - "dev": true, - "peer": true, - "dependencies": { - "@babel/helper-module-transforms": "^7.21.2", - "@babel/helper-plugin-utils": "^7.20.2", - "@babel/helper-simple-access": "^7.20.2" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-modules-systemjs": { - "version": "7.20.11", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.20.11.tgz", - "integrity": "sha512-vVu5g9BPQKSFEmvt2TA4Da5N+QVS66EX21d8uoOihC+OCpUoGvzVsXeqFdtAEfVa5BILAeFt+U7yVmLbQnAJmw==", - "dev": true, - "peer": true, - "dependencies": { - "@babel/helper-hoist-variables": "^7.18.6", - "@babel/helper-module-transforms": "^7.20.11", - "@babel/helper-plugin-utils": "^7.20.2", - "@babel/helper-validator-identifier": "^7.19.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-modules-umd": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.18.6.tgz", - "integrity": "sha512-dcegErExVeXcRqNtkRU/z8WlBLnvD4MRnHgNs3MytRO1Mn1sHRyhbcpYbVMGclAqOjdW+9cfkdZno9dFdfKLfQ==", - "dev": true, - "peer": true, - "dependencies": { - "@babel/helper-module-transforms": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-named-capturing-groups-regex": { - "version": "7.20.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.20.5.tgz", - "integrity": "sha512-mOW4tTzi5iTLnw+78iEq3gr8Aoq4WNRGpmSlrogqaiCBoR1HFhpU4JkpQFOHfeYx3ReVIFWOQJS4aZBRvuZ6mA==", - "dev": true, - "peer": true, - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.20.5", - "@babel/helper-plugin-utils": "^7.20.2" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/plugin-transform-new-target": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.18.6.tgz", - "integrity": "sha512-DjwFA/9Iu3Z+vrAn+8pBUGcjhxKguSMlsFqeCKbhb9BAV756v0krzVK04CRDi/4aqmk8BsHb4a/gFcaA5joXRw==", - "dev": true, - "peer": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-object-super": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.18.6.tgz", - "integrity": "sha512-uvGz6zk+pZoS1aTZrOvrbj6Pp/kK2mp45t2B+bTDre2UgsZZ8EZLSJtUg7m/no0zOJUWgFONpB7Zv9W2tSaFlA==", - "dev": true, - "peer": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6", - "@babel/helper-replace-supers": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-parameters": { - "version": "7.21.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.21.3.tgz", - "integrity": "sha512-Wxc+TvppQG9xWFYatvCGPvZ6+SIUxQ2ZdiBP+PHYMIjnPXD+uThCshaz4NZOnODAtBjjcVQQ/3OKs9LW28purQ==", - "dev": true, - "peer": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.20.2" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-property-literals": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.18.6.tgz", - "integrity": "sha512-cYcs6qlgafTud3PAzrrRNbQtfpQ8+y/+M5tKmksS9+M1ckbH6kzY8MrexEM9mcA6JDsukE19iIRvAyYl463sMg==", - "dev": true, - "peer": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-react-display-name": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.18.6.tgz", - "integrity": "sha512-TV4sQ+T013n61uMoygyMRm+xf04Bd5oqFpv2jAEQwSZ8NwQA7zeRPg1LMVg2PWi3zWBz+CLKD+v5bcpZ/BS0aA==", - "dev": true, - "peer": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-react-jsx": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.18.6.tgz", - "integrity": "sha512-Mz7xMPxoy9kPS/JScj6fJs03TZ/fZ1dJPlMjRAgTaxaS0fUBk8FV/A2rRgfPsVCZqALNwMexD+0Uaf5zlcKPpw==", - "dev": true, - "peer": true, - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.18.6", - "@babel/helper-module-imports": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6", - "@babel/plugin-syntax-jsx": "^7.18.6", - "@babel/types": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-react-jsx-self": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.18.6.tgz", - "integrity": "sha512-A0LQGx4+4Jv7u/tWzoJF7alZwnBDQd6cGLh9P+Ttk4dpiL+J5p7NSNv/9tlEFFJDq3kjxOavWmbm6t0Gk+A3Ig==", - "dev": true, - "peer": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-react-jsx-source": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.18.6.tgz", - "integrity": "sha512-utZmlASneDfdaMh0m/WausbjUjEdGrQJz0vFK93d7wD3xf5wBtX219+q6IlCNZeguIcxS2f/CvLZrlLSvSHQXw==", - "dev": true, - "peer": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-regenerator": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.18.6.tgz", - "integrity": "sha512-poqRI2+qiSdeldcz4wTSTXBRryoq3Gc70ye7m7UD5Ww0nE29IXqMl6r7Nd15WBgRd74vloEMlShtH6CKxVzfmQ==", - "dev": true, - "peer": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6", - "regenerator-transform": "^0.15.0" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-reserved-words": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.18.6.tgz", - "integrity": "sha512-oX/4MyMoypzHjFrT1CdivfKZ+XvIPMFXwwxHp/r0Ddy2Vuomt4HDFGmft1TAY2yiTKiNSsh3kjBAzcM8kSdsjA==", - "dev": true, - "peer": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-runtime": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.18.6.tgz", - "integrity": "sha512-8uRHk9ZmRSnWqUgyae249EJZ94b0yAGLBIqzZzl+0iEdbno55Pmlt/32JZsHwXD9k/uZj18Aqqk35wBX4CBTXA==", - "dev": true, - "peer": true, - "dependencies": { - "@babel/helper-module-imports": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6", - "babel-plugin-polyfill-corejs2": "^0.3.1", - "babel-plugin-polyfill-corejs3": "^0.5.2", - "babel-plugin-polyfill-regenerator": "^0.3.1", - "semver": "^6.3.0" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-shorthand-properties": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.18.6.tgz", - "integrity": "sha512-eCLXXJqv8okzg86ywZJbRn19YJHU4XUa55oz2wbHhaQVn/MM+XhukiT7SYqp/7o00dg52Rj51Ny+Ecw4oyoygw==", - "dev": true, - "peer": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-spread": { - "version": "7.20.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.20.7.tgz", - "integrity": "sha512-ewBbHQ+1U/VnH1fxltbJqDeWBU1oNLG8Dj11uIv3xVf7nrQu0bPGe5Rf716r7K5Qz+SqtAOVswoVunoiBtGhxw==", - "dev": true, - "peer": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.20.2", - "@babel/helper-skip-transparent-expression-wrappers": "^7.20.0" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-sticky-regex": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.18.6.tgz", - "integrity": "sha512-kfiDrDQ+PBsQDO85yj1icueWMfGfJFKN1KCkndygtu/C9+XUfydLC8Iv5UYJqRwy4zk8EcplRxEOeLyjq1gm6Q==", - "dev": true, - "peer": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-template-literals": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.18.9.tgz", - "integrity": "sha512-S8cOWfT82gTezpYOiVaGHrCbhlHgKhQt8XH5ES46P2XWmX92yisoZywf5km75wv5sYcXDUCLMmMxOLCtthDgMA==", - "dev": true, - "peer": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-typeof-symbol": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.18.9.tgz", - "integrity": "sha512-SRfwTtF11G2aemAZWivL7PD+C9z52v9EvMqH9BuYbabyPuKUvSWks3oCg6041pT925L4zVFqaVBeECwsmlguEw==", - "dev": true, - "peer": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-typescript": { - "version": "7.18.8", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.18.8.tgz", - "integrity": "sha512-p2xM8HI83UObjsZGofMV/EdYjamsDm6MoN3hXPYIT0+gxIoopE+B7rPYKAxfrz9K9PK7JafTTjqYC6qipLExYA==", - "dev": true, - "peer": true, - "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6", - "@babel/plugin-syntax-typescript": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-unicode-escapes": { - "version": "7.18.10", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.18.10.tgz", - "integrity": "sha512-kKAdAI+YzPgGY/ftStBFXTI1LZFju38rYThnfMykS+IXy8BVx+res7s2fxf1l8I35DV2T97ezo6+SGrXz6B3iQ==", - "dev": true, - "peer": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-unicode-regex": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.18.6.tgz", - "integrity": "sha512-gE7A6Lt7YLnNOL3Pb9BNeZvi+d8l7tcRrG4+pwJjK9hD2xX4mEvjlQW60G9EEmfXVYRPv9VRQcyegIVHCql/AA==", - "dev": true, - "peer": true, - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/preset-env": { - "version": "7.20.2", - "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.20.2.tgz", - "integrity": "sha512-1G0efQEWR1EHkKvKHqbG+IN/QdgwfByUpM5V5QroDzGV2t3S/WXNQd693cHiHTlCFMpr9B6FkPFXDA2lQcKoDg==", - "dev": true, - "peer": true, - "dependencies": { - "@babel/compat-data": "^7.20.1", - "@babel/helper-compilation-targets": "^7.20.0", - "@babel/helper-plugin-utils": "^7.20.2", - "@babel/helper-validator-option": "^7.18.6", - "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.18.6", - "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.18.9", - "@babel/plugin-proposal-async-generator-functions": "^7.20.1", - "@babel/plugin-proposal-class-properties": "^7.18.6", - "@babel/plugin-proposal-class-static-block": "^7.18.6", - "@babel/plugin-proposal-dynamic-import": "^7.18.6", - "@babel/plugin-proposal-export-namespace-from": "^7.18.9", - "@babel/plugin-proposal-json-strings": "^7.18.6", - "@babel/plugin-proposal-logical-assignment-operators": "^7.18.9", - "@babel/plugin-proposal-nullish-coalescing-operator": "^7.18.6", - "@babel/plugin-proposal-numeric-separator": "^7.18.6", - "@babel/plugin-proposal-object-rest-spread": "^7.20.2", - "@babel/plugin-proposal-optional-catch-binding": "^7.18.6", - "@babel/plugin-proposal-optional-chaining": "^7.18.9", - "@babel/plugin-proposal-private-methods": "^7.18.6", - "@babel/plugin-proposal-private-property-in-object": "^7.18.6", - "@babel/plugin-proposal-unicode-property-regex": "^7.18.6", - "@babel/plugin-syntax-async-generators": "^7.8.4", - "@babel/plugin-syntax-class-properties": "^7.12.13", - "@babel/plugin-syntax-class-static-block": "^7.14.5", - "@babel/plugin-syntax-dynamic-import": "^7.8.3", - "@babel/plugin-syntax-export-namespace-from": "^7.8.3", - "@babel/plugin-syntax-import-assertions": "^7.20.0", - "@babel/plugin-syntax-json-strings": "^7.8.3", - "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", - "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", - "@babel/plugin-syntax-numeric-separator": "^7.10.4", - "@babel/plugin-syntax-object-rest-spread": "^7.8.3", - "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", - "@babel/plugin-syntax-optional-chaining": "^7.8.3", - "@babel/plugin-syntax-private-property-in-object": "^7.14.5", - "@babel/plugin-syntax-top-level-await": "^7.14.5", - "@babel/plugin-transform-arrow-functions": "^7.18.6", - "@babel/plugin-transform-async-to-generator": "^7.18.6", - "@babel/plugin-transform-block-scoped-functions": "^7.18.6", - "@babel/plugin-transform-block-scoping": "^7.20.2", - "@babel/plugin-transform-classes": "^7.20.2", - "@babel/plugin-transform-computed-properties": "^7.18.9", - "@babel/plugin-transform-destructuring": "^7.20.2", - "@babel/plugin-transform-dotall-regex": "^7.18.6", - "@babel/plugin-transform-duplicate-keys": "^7.18.9", - "@babel/plugin-transform-exponentiation-operator": "^7.18.6", - "@babel/plugin-transform-for-of": "^7.18.8", - "@babel/plugin-transform-function-name": "^7.18.9", - "@babel/plugin-transform-literals": "^7.18.9", - "@babel/plugin-transform-member-expression-literals": "^7.18.6", - "@babel/plugin-transform-modules-amd": "^7.19.6", - "@babel/plugin-transform-modules-commonjs": "^7.19.6", - "@babel/plugin-transform-modules-systemjs": "^7.19.6", - "@babel/plugin-transform-modules-umd": "^7.18.6", - "@babel/plugin-transform-named-capturing-groups-regex": "^7.19.1", - "@babel/plugin-transform-new-target": "^7.18.6", - "@babel/plugin-transform-object-super": "^7.18.6", - "@babel/plugin-transform-parameters": "^7.20.1", - "@babel/plugin-transform-property-literals": "^7.18.6", - "@babel/plugin-transform-regenerator": "^7.18.6", - "@babel/plugin-transform-reserved-words": "^7.18.6", - "@babel/plugin-transform-shorthand-properties": "^7.18.6", - "@babel/plugin-transform-spread": "^7.19.0", - "@babel/plugin-transform-sticky-regex": "^7.18.6", - "@babel/plugin-transform-template-literals": "^7.18.9", - "@babel/plugin-transform-typeof-symbol": "^7.18.9", - "@babel/plugin-transform-unicode-escapes": "^7.18.10", - "@babel/plugin-transform-unicode-regex": "^7.18.6", - "@babel/preset-modules": "^0.1.5", - "@babel/types": "^7.20.2", - "babel-plugin-polyfill-corejs2": "^0.3.3", - "babel-plugin-polyfill-corejs3": "^0.6.0", - "babel-plugin-polyfill-regenerator": "^0.4.1", - "core-js-compat": "^3.25.1", - "semver": "^6.3.0" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/preset-env/node_modules/babel-plugin-polyfill-corejs3": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.6.0.tgz", - "integrity": "sha512-+eHqR6OPcBhJOGgsIar7xoAB1GcSwVUA3XjAd7HJNzOXT4wv6/H7KIdA/Nc60cvUlDbKApmqNvD1B1bzOt4nyA==", - "dev": true, - "peer": true, - "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.3.3", - "core-js-compat": "^3.25.1" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/preset-env/node_modules/babel-plugin-polyfill-regenerator": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.4.1.tgz", - "integrity": "sha512-NtQGmyQDXjQqQ+IzRkBVwEOz9lQ4zxAQZgoAYEtU9dJjnl1Oc98qnN7jcp+bE7O7aYzVpavXE3/VKXNzUbh7aw==", - "dev": true, - "peer": true, - "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.3.3" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/preset-flow": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/preset-flow/-/preset-flow-7.18.6.tgz", - "integrity": "sha512-E7BDhL64W6OUqpuyHnSroLnqyRTcG6ZdOBl1OKI/QK/HJfplqK/S3sq1Cckx7oTodJ5yOXyfw7rEADJ6UjoQDQ==", - "dev": true, - "peer": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6", - "@babel/helper-validator-option": "^7.18.6", - "@babel/plugin-transform-flow-strip-types": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/preset-modules": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.5.tgz", - "integrity": "sha512-A57th6YRG7oR3cq/yt/Y84MvGgE0eJG2F1JLhKuyG+jFxEgrd/HAMJatiFtmOiZurz+0DkrvbheCLaV5f2JfjA==", - "dev": true, - "peer": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.0.0", - "@babel/plugin-proposal-unicode-property-regex": "^7.4.4", - "@babel/plugin-transform-dotall-regex": "^7.4.4", - "@babel/types": "^7.4.4", - "esutils": "^2.0.2" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/preset-typescript": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.18.6.tgz", - "integrity": "sha512-s9ik86kXBAnD760aybBucdpnLsAt0jK1xqJn2juOn9lkOvSHV60os5hxoVJsPzMQxvnUJFAlkont2DvvaYEBtQ==", - "dev": true, - "peer": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6", - "@babel/helper-validator-option": "^7.18.6", - "@babel/plugin-transform-typescript": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/register": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/register/-/register-7.18.6.tgz", - "integrity": "sha512-tkYtONzaO8rQubZzpBnvZPFcHgh8D9F55IjOsYton4X2IBoyRn2ZSWQqySTZnUn2guZbxbQiAB27hJEbvXamhQ==", - "dev": true, - "peer": true, - "dependencies": { - "clone-deep": "^4.0.1", - "find-cache-dir": "^2.0.0", - "make-dir": "^2.1.0", - "pirates": "^4.0.5", - "source-map-support": "^0.5.16" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/regjsgen": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/@babel/regjsgen/-/regjsgen-0.8.0.tgz", - "integrity": "sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA==", - "dev": true, - "peer": true - }, "node_modules/@babel/runtime": { "version": "7.21.0", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.21.0.tgz", @@ -2212,9 +581,9 @@ } }, "node_modules/@cypress/request": { - "version": "2.88.11", - "resolved": "https://registry.npmjs.org/@cypress/request/-/request-2.88.11.tgz", - "integrity": "sha512-M83/wfQ1EkspjkE2lNWNV5ui2Cv7UCv1swW1DqljahbzLVWltcsexQh8jYtuS/vzFXP+HySntGM83ZXA9fn17w==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@cypress/request/-/request-3.0.1.tgz", + "integrity": "sha512-TWivJlJi8ZDx2wGOw1dbLuHJKUYX7bWySw377nlnGOW3hP9/MUKIsEdXT/YngWxVdgNCHRBmFlBipE+5/2ZZlQ==", "dev": true, "dependencies": { "aws-sign2": "~0.7.0", @@ -2230,9 +599,9 @@ "json-stringify-safe": "~5.0.1", "mime-types": "~2.1.19", "performance-now": "^2.1.0", - "qs": "~6.10.3", + "qs": "6.10.4", "safe-buffer": "^5.1.2", - "tough-cookie": "~2.5.0", + "tough-cookie": "^4.1.3", "tunnel-agent": "^0.6.0", "uuid": "^8.3.2" }, @@ -2284,23 +653,23 @@ } }, "node_modules/@eslint-community/regexpp": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.4.0.tgz", - "integrity": "sha512-A9983Q0LnDGdLPjxyXQ00sbV+K+O+ko2Dr+CZigbHWtX9pNfxlaBkMR8X1CztI73zuEyEBXTVjx7CE+/VSwDiQ==", + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.8.1.tgz", + "integrity": "sha512-PWiOzLIUAjN/w5K17PoF4n6sKBw0gqLHPhywmYHP4t1VFQQVYeb1yWsJwnMVEMl3tUHME7X/SJPZLmtG7XBDxQ==", "dev": true, "engines": { "node": "^12.0.0 || ^14.0.0 || >=16.0.0" } }, "node_modules/@eslint/eslintrc": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.0.2.tgz", - "integrity": "sha512-3W4f5tDUra+pA+FzgugqL2pRimUTDJWKr7BINqOpkZrC0uYI0NIc0/JFgBROCU07HR6GieA5m3/rsPIhDmCXTQ==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.2.tgz", + "integrity": "sha512-+wvgpDsrB1YqAMdEUCcnTlpfVBH7Vqn6A/NT3D8WVXFIaKMlErPIZT3oCIAVCOtarRpMtelZLqJeU3t7WY6X6g==", "dev": true, "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", - "espree": "^9.5.1", + "espree": "^9.6.0", "globals": "^13.19.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", @@ -2316,9 +685,9 @@ } }, "node_modules/@eslint/eslintrc/node_modules/globals": { - "version": "13.20.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.20.0.tgz", - "integrity": "sha512-Qg5QtVkCy/kv3FUSlu4ukeZDVf9ee0iXLAUYX13gbR17bnejFTzr4iS9bY7kwCf1NztRNm1t91fjOiyx4CSwPQ==", + "version": "13.21.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.21.0.tgz", + "integrity": "sha512-ybyme3s4yy/t/3s35bewwXKOf7cvzfreG2lH0lZl0JB7I4GxRP2ghxOK/Nb9EkRXdbBXZLfq/p/0W2JUONB/Gg==", "dev": true, "dependencies": { "type-fest": "^0.20.2" @@ -2368,35 +737,18 @@ } }, "node_modules/@eslint/js": { - "version": "8.39.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.39.0.tgz", - "integrity": "sha512-kf9RB0Fg7NZfap83B3QOqOGg9QmD9yBudqQXzzOtn3i4y7ZUXe5ONeW34Gwi+TxhH4mvj72R1Zc300KUMa9Bng==", + "version": "8.50.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.50.0.tgz", + "integrity": "sha512-NCC3zz2+nvYd+Ckfh87rA47zfu2QsQpvc6k1yzTk+b9KzRj0wkGa8LSoGOXN6Zv4lRf/EIoZ80biDh9HOI+RNQ==", "dev": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, - "node_modules/@hapi/hoek": { - "version": "9.3.0", - "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz", - "integrity": "sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==", - "dev": true, - "peer": true - }, - "node_modules/@hapi/topo": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-5.1.0.tgz", - "integrity": "sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==", - "dev": true, - "peer": true, - "dependencies": { - "@hapi/hoek": "^9.0.0" - } - }, "node_modules/@humanwhocodes/config-array": { - "version": "0.11.8", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.8.tgz", - "integrity": "sha512-UybHIJzJnR5Qc/MsD9Kr+RpO2h+/P1GhOwdiLPXK5TWk5sgTdu88bTD9UP+CKbPPh5Rni1u0GjAdYQLemG8g+g==", + "version": "0.11.11", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.11.tgz", + "integrity": "sha512-N2brEuAadi0CcdeMXUkhbZB84eskAc8MEX1By6qEchoVywSgXPIjou4rYsl0V3Hj0ZnuGycGCjdNgockbzeWNA==", "dev": true, "dependencies": { "@humanwhocodes/object-schema": "^1.2.1", @@ -2426,36 +778,6 @@ "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", "dev": true }, - "node_modules/@jest/create-cache-key-function": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/@jest/create-cache-key-function/-/create-cache-key-function-27.5.1.tgz", - "integrity": "sha512-dmH1yW+makpTSURTy8VzdUwFnfQh1G8R+DxO2Ho2FFmBbKFEVm+3jWdvFhE2VqB/LATCTokkP0dotjyQyw5/AQ==", - "dev": true, - "peer": true, - "dependencies": { - "@jest/types": "^27.5.1" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/@jest/types": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-27.5.1.tgz", - "integrity": "sha512-Cx46iJ9QpwQTjIdq5VJu2QTMMs3QlEjI0x1QbBP5W1+nMzyc2XmimiRR/CbX9TO0cPTeUlxWMOu8mslYsJ8DEw==", - "dev": true, - "peer": true, - "dependencies": { - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^16.0.0", - "chalk": "^4.0.0" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, "node_modules/@jridgewell/gen-mapping": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.1.1.tgz", @@ -2534,10 +856,42 @@ "@jridgewell/sourcemap-codec": "1.4.14" } }, + "node_modules/@mattermost/client": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/@mattermost/client/-/client-9.0.0.tgz", + "integrity": "sha512-hXeOKvqUtuOC7/iFrGigDwKGvXsxdyLJgveU5SOwxQsi0fIf13D1a3IZH8xt6Df6icXp14yIVKD20t1UuHIc1g==", + "dev": true, + "dependencies": { + "form-data": "^4.0.0" + }, + "peerDependencies": { + "@mattermost/types": "*", + "typescript": "^4.3" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@mattermost/client/node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dev": true, + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/@mattermost/types": { - "version": "7.10.0", - "resolved": "https://registry.npmjs.org/@mattermost/types/-/types-7.10.0.tgz", - "integrity": "sha512-nPJThc+BHIUGmqN5GMTCbFiETxxT7pD7hoEM3N+z3l6bRrdTcLaK2NVmbSjEhi6jN1jDoFtD9ieCiCPg5pT4EA==", + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/@mattermost/types/-/types-9.0.0.tgz", + "integrity": "sha512-pRqls7j3/oEQQEhZYv6p7yfqN8k+XMRvFIOyeiy3ov0Vwcm7JTRCH8qT+XmqTigVItvVRiUyzpdN9JWj2Rb5yQ==", "dev": true, "peerDependencies": { "typescript": "^4.3" @@ -2592,1156 +946,58 @@ "node": ">= 8" } }, - "node_modules/@react-native-community/cli": { - "version": "8.0.3", - "resolved": "https://registry.npmjs.org/@react-native-community/cli/-/cli-8.0.3.tgz", - "integrity": "sha512-7gY7QCEdpYDbvbdZBt6w64YPExLoiUpH/lVRaR4tKl6JalqXzrUotOJnBOS/qEC4q0nk0WXsiC5EkuiSliKS5Q==", - "dev": true, - "peer": true, - "dependencies": { - "@react-native-community/cli-clean": "^8.0.0", - "@react-native-community/cli-config": "^8.0.3", - "@react-native-community/cli-debugger-ui": "^8.0.0", - "@react-native-community/cli-doctor": "^8.0.3", - "@react-native-community/cli-hermes": "^8.0.2", - "@react-native-community/cli-plugin-metro": "^8.0.0", - "@react-native-community/cli-server-api": "^8.0.0", - "@react-native-community/cli-tools": "^8.0.0", - "@react-native-community/cli-types": "^8.0.0", - "chalk": "^4.1.2", - "commander": "^2.19.0", - "execa": "^1.0.0", - "find-up": "^4.1.0", - "fs-extra": "^8.1.0", - "graceful-fs": "^4.1.3", - "leven": "^3.1.0", - "lodash": "^4.17.15", - "minimist": "^1.2.0", - "prompts": "^2.4.0", - "semver": "^6.3.0" - }, - "bin": { - "react-native": "build/bin.js" - }, - "engines": { - "node": ">=12" - }, - "peerDependencies": { - "react-native": "*" - } - }, - "node_modules/@react-native-community/cli-clean": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/@react-native-community/cli-clean/-/cli-clean-8.0.0.tgz", - "integrity": "sha512-VY/kwyH5xp6oXiB9bcwa+I9W5k6WR/nX3s85FuMW76hSlgG1UVAGL04uZPwYlSmMZuSOSuoXOaIjJ7wAvQMBpg==", - "dev": true, - "peer": true, - "dependencies": { - "@react-native-community/cli-tools": "^8.0.0", - "chalk": "^4.1.2", - "execa": "^1.0.0", - "prompts": "^2.4.0" - } - }, - "node_modules/@react-native-community/cli-clean/node_modules/cross-spawn": { - "version": "6.0.5", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", - "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", - "dev": true, - "peer": true, - "dependencies": { - "nice-try": "^1.0.4", - "path-key": "^2.0.1", - "semver": "^5.5.0", - "shebang-command": "^1.2.0", - "which": "^1.2.9" - }, - "engines": { - "node": ">=4.8" - } - }, - "node_modules/@react-native-community/cli-clean/node_modules/execa": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", - "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", - "dev": true, - "peer": true, - "dependencies": { - "cross-spawn": "^6.0.0", - "get-stream": "^4.0.0", - "is-stream": "^1.1.0", - "npm-run-path": "^2.0.0", - "p-finally": "^1.0.0", - "signal-exit": "^3.0.0", - "strip-eof": "^1.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/@react-native-community/cli-clean/node_modules/get-stream": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", - "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", - "dev": true, - "peer": true, - "dependencies": { - "pump": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/@react-native-community/cli-clean/node_modules/is-stream": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", - "integrity": "sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==", - "dev": true, - "peer": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/@react-native-community/cli-clean/node_modules/npm-run-path": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", - "integrity": "sha512-lJxZYlT4DW/bRUtFh1MQIWqmLwQfAxnqWG4HhEdjMlkrJYnJn0Jrr2u3mgxqaWsdiBc76TYkTG/mhrnYTuzfHw==", - "dev": true, - "peer": true, - "dependencies": { - "path-key": "^2.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@react-native-community/cli-clean/node_modules/path-key": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", - "integrity": "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==", - "dev": true, - "peer": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/@react-native-community/cli-clean/node_modules/semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true, - "peer": true, - "bin": { - "semver": "bin/semver" - } - }, - "node_modules/@react-native-community/cli-clean/node_modules/shebang-command": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", - "integrity": "sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==", - "dev": true, - "peer": true, - "dependencies": { - "shebang-regex": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/@react-native-community/cli-clean/node_modules/shebang-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", - "integrity": "sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==", - "dev": true, - "peer": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/@react-native-community/cli-clean/node_modules/which": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", - "dev": true, - "peer": true, - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "which": "bin/which" - } - }, - "node_modules/@react-native-community/cli-config": { - "version": "8.0.3", - "resolved": "https://registry.npmjs.org/@react-native-community/cli-config/-/cli-config-8.0.3.tgz", - "integrity": "sha512-QhLU6QZywkoO4FzpeEkdoYml0nE9tBwhmOUI/c5iYPOtKhhXiW8kNCLiX96TJDiZonalzptkkNiRZkipdz/8hw==", - "dev": true, - "peer": true, - "dependencies": { - "@react-native-community/cli-tools": "^8.0.0", - "cosmiconfig": "^5.1.0", - "deepmerge": "^3.2.0", - "glob": "^7.1.3", - "joi": "^17.2.1" - } - }, - "node_modules/@react-native-community/cli-config/node_modules/deepmerge": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-3.3.0.tgz", - "integrity": "sha512-GRQOafGHwMHpjPx9iCvTgpu9NojZ49q794EEL94JVEw6VaeA8XTUyBKvAkOOjBX9oJNiV6G3P+T+tihFjo2TqA==", - "dev": true, - "peer": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/@react-native-community/cli-debugger-ui": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/@react-native-community/cli-debugger-ui/-/cli-debugger-ui-8.0.0.tgz", - "integrity": "sha512-u2jq06GZwZ9sRERzd9FIgpW6yv4YOW4zz7Ym/B8eSzviLmy3yI/8mxJtvlGW+J8lBsfMcQoqJpqI6Rl1nZy9yQ==", - "dev": true, - "peer": true, - "dependencies": { - "serve-static": "^1.13.1" - } - }, - "node_modules/@react-native-community/cli-doctor": { - "version": "8.0.3", - "resolved": "https://registry.npmjs.org/@react-native-community/cli-doctor/-/cli-doctor-8.0.3.tgz", - "integrity": "sha512-ndISZhZqOoeSuQCm5KLwJNkckk14Bqn1N8LHJbC6l4zAyDU0nQRO1IVPoV5uyaANMzMqSNzS6k9N4M0PpcuhIQ==", - "dev": true, - "peer": true, - "dependencies": { - "@react-native-community/cli-config": "^8.0.3", - "@react-native-community/cli-platform-ios": "^8.0.2", - "@react-native-community/cli-tools": "^8.0.0", - "chalk": "^4.1.2", - "command-exists": "^1.2.8", - "envinfo": "^7.7.2", - "execa": "^1.0.0", - "hermes-profile-transformer": "^0.0.6", - "ip": "^1.1.5", - "node-stream-zip": "^1.9.1", - "ora": "^5.4.1", - "prompts": "^2.4.0", - "semver": "^6.3.0", - "strip-ansi": "^5.2.0", - "sudo-prompt": "^9.0.0", - "wcwidth": "^1.0.1" - } - }, - "node_modules/@react-native-community/cli-doctor/node_modules/cross-spawn": { - "version": "6.0.5", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", - "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", - "dev": true, - "peer": true, - "dependencies": { - "nice-try": "^1.0.4", - "path-key": "^2.0.1", - "semver": "^5.5.0", - "shebang-command": "^1.2.0", - "which": "^1.2.9" - }, - "engines": { - "node": ">=4.8" - } - }, - "node_modules/@react-native-community/cli-doctor/node_modules/cross-spawn/node_modules/semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true, - "peer": true, - "bin": { - "semver": "bin/semver" - } - }, - "node_modules/@react-native-community/cli-doctor/node_modules/execa": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", - "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", - "dev": true, - "peer": true, - "dependencies": { - "cross-spawn": "^6.0.0", - "get-stream": "^4.0.0", - "is-stream": "^1.1.0", - "npm-run-path": "^2.0.0", - "p-finally": "^1.0.0", - "signal-exit": "^3.0.0", - "strip-eof": "^1.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/@react-native-community/cli-doctor/node_modules/get-stream": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", - "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", - "dev": true, - "peer": true, - "dependencies": { - "pump": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/@react-native-community/cli-doctor/node_modules/is-stream": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", - "integrity": "sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==", - "dev": true, - "peer": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/@react-native-community/cli-doctor/node_modules/npm-run-path": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", - "integrity": "sha512-lJxZYlT4DW/bRUtFh1MQIWqmLwQfAxnqWG4HhEdjMlkrJYnJn0Jrr2u3mgxqaWsdiBc76TYkTG/mhrnYTuzfHw==", - "dev": true, - "peer": true, - "dependencies": { - "path-key": "^2.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@react-native-community/cli-doctor/node_modules/path-key": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", - "integrity": "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==", - "dev": true, - "peer": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/@react-native-community/cli-doctor/node_modules/shebang-command": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", - "integrity": "sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==", - "dev": true, - "peer": true, - "dependencies": { - "shebang-regex": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/@react-native-community/cli-doctor/node_modules/shebang-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", - "integrity": "sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==", - "dev": true, - "peer": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/@react-native-community/cli-doctor/node_modules/which": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", - "dev": true, - "peer": true, - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "which": "bin/which" - } - }, - "node_modules/@react-native-community/cli-hermes": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/@react-native-community/cli-hermes/-/cli-hermes-8.0.2.tgz", - "integrity": "sha512-RZ9uHTf3UFtGTYxq88uENJEdaDB8ab+YPBDn+Li1u78IKwNeL04F0A1A3ab3hYUkG4PEPnL2rkYSlzzNFLOSPQ==", - "dev": true, - "peer": true, - "dependencies": { - "@react-native-community/cli-platform-android": "^8.0.2", - "@react-native-community/cli-tools": "^8.0.0", - "chalk": "^4.1.2", - "hermes-profile-transformer": "^0.0.6", - "ip": "^1.1.5" - } - }, - "node_modules/@react-native-community/cli-platform-android": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/@react-native-community/cli-platform-android/-/cli-platform-android-8.0.2.tgz", - "integrity": "sha512-pAEkt+GULesr8FphTpaNYSmu+O1CPQ2zCXkAg4kRd0WXpq3BsVqomyDWd/eMXTkY/yYQMGl6KilU2p9r/hnfhA==", - "dev": true, - "peer": true, - "dependencies": { - "@react-native-community/cli-tools": "^8.0.0", - "chalk": "^4.1.2", - "execa": "^1.0.0", - "fs-extra": "^8.1.0", - "glob": "^7.1.3", - "jetifier": "^1.6.2", - "lodash": "^4.17.15", - "logkitty": "^0.7.1", - "slash": "^3.0.0" - } - }, - "node_modules/@react-native-community/cli-platform-android/node_modules/cross-spawn": { - "version": "6.0.5", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", - "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", - "dev": true, - "peer": true, - "dependencies": { - "nice-try": "^1.0.4", - "path-key": "^2.0.1", - "semver": "^5.5.0", - "shebang-command": "^1.2.0", - "which": "^1.2.9" - }, - "engines": { - "node": ">=4.8" - } - }, - "node_modules/@react-native-community/cli-platform-android/node_modules/execa": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", - "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", - "dev": true, - "peer": true, - "dependencies": { - "cross-spawn": "^6.0.0", - "get-stream": "^4.0.0", - "is-stream": "^1.1.0", - "npm-run-path": "^2.0.0", - "p-finally": "^1.0.0", - "signal-exit": "^3.0.0", - "strip-eof": "^1.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/@react-native-community/cli-platform-android/node_modules/fs-extra": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", - "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", - "dev": true, - "peer": true, - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^4.0.0", - "universalify": "^0.1.0" - }, - "engines": { - "node": ">=6 <7 || >=8" - } - }, - "node_modules/@react-native-community/cli-platform-android/node_modules/get-stream": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", - "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", - "dev": true, - "peer": true, - "dependencies": { - "pump": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/@react-native-community/cli-platform-android/node_modules/is-stream": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", - "integrity": "sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==", - "dev": true, - "peer": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/@react-native-community/cli-platform-android/node_modules/jsonfile": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", - "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", - "dev": true, - "peer": true, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/@react-native-community/cli-platform-android/node_modules/npm-run-path": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", - "integrity": "sha512-lJxZYlT4DW/bRUtFh1MQIWqmLwQfAxnqWG4HhEdjMlkrJYnJn0Jrr2u3mgxqaWsdiBc76TYkTG/mhrnYTuzfHw==", - "dev": true, - "peer": true, - "dependencies": { - "path-key": "^2.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@react-native-community/cli-platform-android/node_modules/path-key": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", - "integrity": "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==", - "dev": true, - "peer": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/@react-native-community/cli-platform-android/node_modules/semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true, - "peer": true, - "bin": { - "semver": "bin/semver" - } - }, - "node_modules/@react-native-community/cli-platform-android/node_modules/shebang-command": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", - "integrity": "sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==", - "dev": true, - "peer": true, - "dependencies": { - "shebang-regex": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/@react-native-community/cli-platform-android/node_modules/shebang-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", - "integrity": "sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==", - "dev": true, - "peer": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/@react-native-community/cli-platform-android/node_modules/universalify": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", - "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", - "dev": true, - "peer": true, - "engines": { - "node": ">= 4.0.0" - } - }, - "node_modules/@react-native-community/cli-platform-android/node_modules/which": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", - "dev": true, - "peer": true, - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "which": "bin/which" - } - }, - "node_modules/@react-native-community/cli-platform-ios": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/@react-native-community/cli-platform-ios/-/cli-platform-ios-8.0.2.tgz", - "integrity": "sha512-LxWzj6jIZr5Ot893TKFbt0/T3WkVe6pbc/FSTo+TDQq1FQr/Urv16Uqn0AcL4IX2O1g3Qd13d0vtR/Cdpn3VNw==", - "dev": true, - "peer": true, - "dependencies": { - "@react-native-community/cli-tools": "^8.0.0", - "chalk": "^4.1.2", - "execa": "^1.0.0", - "glob": "^7.1.3", - "js-yaml": "^3.13.1", - "lodash": "^4.17.15", - "ora": "^5.4.1", - "plist": "^3.0.2" - } - }, - "node_modules/@react-native-community/cli-platform-ios/node_modules/argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "dev": true, - "peer": true, - "dependencies": { - "sprintf-js": "~1.0.2" - } - }, - "node_modules/@react-native-community/cli-platform-ios/node_modules/cross-spawn": { - "version": "6.0.5", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", - "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", - "dev": true, - "peer": true, - "dependencies": { - "nice-try": "^1.0.4", - "path-key": "^2.0.1", - "semver": "^5.5.0", - "shebang-command": "^1.2.0", - "which": "^1.2.9" - }, - "engines": { - "node": ">=4.8" - } - }, - "node_modules/@react-native-community/cli-platform-ios/node_modules/execa": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", - "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", - "dev": true, - "peer": true, - "dependencies": { - "cross-spawn": "^6.0.0", - "get-stream": "^4.0.0", - "is-stream": "^1.1.0", - "npm-run-path": "^2.0.0", - "p-finally": "^1.0.0", - "signal-exit": "^3.0.0", - "strip-eof": "^1.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/@react-native-community/cli-platform-ios/node_modules/get-stream": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", - "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", - "dev": true, - "peer": true, - "dependencies": { - "pump": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/@react-native-community/cli-platform-ios/node_modules/is-stream": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", - "integrity": "sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==", - "dev": true, - "peer": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/@react-native-community/cli-platform-ios/node_modules/js-yaml": { - "version": "3.14.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", - "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", - "dev": true, - "peer": true, - "dependencies": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/@react-native-community/cli-platform-ios/node_modules/npm-run-path": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", - "integrity": "sha512-lJxZYlT4DW/bRUtFh1MQIWqmLwQfAxnqWG4HhEdjMlkrJYnJn0Jrr2u3mgxqaWsdiBc76TYkTG/mhrnYTuzfHw==", - "dev": true, - "peer": true, - "dependencies": { - "path-key": "^2.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@react-native-community/cli-platform-ios/node_modules/path-key": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", - "integrity": "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==", - "dev": true, - "peer": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/@react-native-community/cli-platform-ios/node_modules/semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true, - "peer": true, - "bin": { - "semver": "bin/semver" - } - }, - "node_modules/@react-native-community/cli-platform-ios/node_modules/shebang-command": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", - "integrity": "sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==", - "dev": true, - "peer": true, - "dependencies": { - "shebang-regex": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/@react-native-community/cli-platform-ios/node_modules/shebang-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", - "integrity": "sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==", - "dev": true, - "peer": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/@react-native-community/cli-platform-ios/node_modules/which": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", - "dev": true, - "peer": true, - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "which": "bin/which" - } - }, - "node_modules/@react-native-community/cli-plugin-metro": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/@react-native-community/cli-plugin-metro/-/cli-plugin-metro-8.0.0.tgz", - "integrity": "sha512-eIowV2ZRbzIWL3RIKVUUSahntXTuAeKzBSsFuhffLZphsV+UdKdtg5ATR9zbq7nsKap4ZseO5DkVqZngUkC7iQ==", - "dev": true, - "peer": true, - "dependencies": { - "@react-native-community/cli-server-api": "^8.0.0", - "@react-native-community/cli-tools": "^8.0.0", - "chalk": "^4.1.2", - "metro": "^0.70.1", - "metro-config": "^0.70.1", - "metro-core": "^0.70.1", - "metro-react-native-babel-transformer": "^0.70.1", - "metro-resolver": "^0.70.1", - "metro-runtime": "^0.70.1", - "readline": "^1.3.0" - } - }, - "node_modules/@react-native-community/cli-server-api": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/@react-native-community/cli-server-api/-/cli-server-api-8.0.0.tgz", - "integrity": "sha512-TxUs3sMl9clt7sdv30XETc6VRzyaEli2vDrk3TB5W5o5nSd1PmQdP4ccdGLO/nDRXwOy72QmmXlYWMg1XGU0Gg==", - "dev": true, - "peer": true, - "dependencies": { - "@react-native-community/cli-debugger-ui": "^8.0.0", - "@react-native-community/cli-tools": "^8.0.0", - "compression": "^1.7.1", - "connect": "^3.6.5", - "errorhandler": "^1.5.0", - "nocache": "^3.0.1", - "pretty-format": "^26.6.2", - "serve-static": "^1.13.1", - "ws": "^7.5.1" - } - }, - "node_modules/@react-native-community/cli-server-api/node_modules/ws": { - "version": "7.5.8", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.8.tgz", - "integrity": "sha512-ri1Id1WinAX5Jqn9HejiGb8crfRio0Qgu8+MtL36rlTA6RLsMdWt1Az/19A2Qij6uSHUMphEFaTKa4WG+UNHNw==", - "dev": true, - "peer": true, - "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 - } - } - }, - "node_modules/@react-native-community/cli-tools": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/@react-native-community/cli-tools/-/cli-tools-8.0.0.tgz", - "integrity": "sha512-jA4y8CebrRZaOJFjc5zMOnls4KfHkBl2FUtBZV2vcWuedQHa6JVwo+KO88ta3Ysby3uY0+mrZagZfXk7c0mrBw==", - "dev": true, - "peer": true, - "dependencies": { - "appdirsjs": "^1.2.4", - "chalk": "^4.1.2", - "lodash": "^4.17.15", - "mime": "^2.4.1", - "node-fetch": "^2.6.0", - "open": "^6.2.0", - "ora": "^5.4.1", - "semver": "^6.3.0", - "shell-quote": "^1.7.3" - } - }, - "node_modules/@react-native-community/cli-tools/node_modules/mime": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", - "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", - "dev": true, - "peer": true, - "bin": { - "mime": "cli.js" - }, - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/@react-native-community/cli-types": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/@react-native-community/cli-types/-/cli-types-8.0.0.tgz", - "integrity": "sha512-1lZS1PEvMlFaN3Se1ksyoFWzMjk+YfKi490GgsqKJln9gvFm8tqVPdnXttI5Uf2DQf3BMse8Bk8dNH4oV6Ewow==", - "dev": true, - "peer": true, - "dependencies": { - "joi": "^17.2.1" - } - }, - "node_modules/@react-native-community/cli/node_modules/commander": { - "version": "2.20.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", - "dev": true, - "peer": true - }, - "node_modules/@react-native-community/cli/node_modules/cross-spawn": { - "version": "6.0.5", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", - "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", - "dev": true, - "peer": true, - "dependencies": { - "nice-try": "^1.0.4", - "path-key": "^2.0.1", - "semver": "^5.5.0", - "shebang-command": "^1.2.0", - "which": "^1.2.9" - }, - "engines": { - "node": ">=4.8" - } - }, - "node_modules/@react-native-community/cli/node_modules/cross-spawn/node_modules/semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true, - "peer": true, - "bin": { - "semver": "bin/semver" - } - }, - "node_modules/@react-native-community/cli/node_modules/execa": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", - "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", - "dev": true, - "peer": true, - "dependencies": { - "cross-spawn": "^6.0.0", - "get-stream": "^4.0.0", - "is-stream": "^1.1.0", - "npm-run-path": "^2.0.0", - "p-finally": "^1.0.0", - "signal-exit": "^3.0.0", - "strip-eof": "^1.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/@react-native-community/cli/node_modules/find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, - "peer": true, - "dependencies": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@react-native-community/cli/node_modules/fs-extra": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", - "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", - "dev": true, - "peer": true, - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^4.0.0", - "universalify": "^0.1.0" - }, - "engines": { - "node": ">=6 <7 || >=8" - } - }, - "node_modules/@react-native-community/cli/node_modules/get-stream": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", - "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", - "dev": true, - "peer": true, - "dependencies": { - "pump": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/@react-native-community/cli/node_modules/is-stream": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", - "integrity": "sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==", - "dev": true, - "peer": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/@react-native-community/cli/node_modules/jsonfile": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", - "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", - "dev": true, - "peer": true, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/@react-native-community/cli/node_modules/locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dev": true, - "peer": true, - "dependencies": { - "p-locate": "^4.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@react-native-community/cli/node_modules/npm-run-path": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", - "integrity": "sha512-lJxZYlT4DW/bRUtFh1MQIWqmLwQfAxnqWG4HhEdjMlkrJYnJn0Jrr2u3mgxqaWsdiBc76TYkTG/mhrnYTuzfHw==", - "dev": true, - "peer": true, - "dependencies": { - "path-key": "^2.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@react-native-community/cli/node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dev": true, - "peer": true, - "dependencies": { - "p-try": "^2.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@react-native-community/cli/node_modules/p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dev": true, - "peer": true, - "dependencies": { - "p-limit": "^2.2.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@react-native-community/cli/node_modules/path-key": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", - "integrity": "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==", - "dev": true, - "peer": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/@react-native-community/cli/node_modules/shebang-command": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", - "integrity": "sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==", - "dev": true, - "peer": true, - "dependencies": { - "shebang-regex": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/@react-native-community/cli/node_modules/shebang-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", - "integrity": "sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==", - "dev": true, - "peer": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/@react-native-community/cli/node_modules/universalify": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", - "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", - "dev": true, - "peer": true, - "engines": { - "node": ">= 4.0.0" - } - }, - "node_modules/@react-native-community/cli/node_modules/which": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", - "dev": true, - "peer": true, - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "which": "bin/which" - } - }, - "node_modules/@react-native-community/netinfo": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/@react-native-community/netinfo/-/netinfo-4.7.0.tgz", - "integrity": "sha512-a/sDB+AsLEUNmhAUlAaTYeXKyQdFGBUfatqKkX5jluBo2CB3OAuTHfm7rSjcaLB9EmG5iSq3fOTpync2E7EYTA==", - "dev": true, - "peerDependencies": { - "react-native": ">=0.59" - } - }, - "node_modules/@react-native/assets": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@react-native/assets/-/assets-1.0.0.tgz", - "integrity": "sha512-KrwSpS1tKI70wuKl68DwJZYEvXktDHdZMG0k2AXD/rJVSlB23/X2CB2cutVR0HwNMJIal9HOUOBB2rVfa6UGtQ==", - "dev": true, - "peer": true - }, - "node_modules/@react-native/normalize-color": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@react-native/normalize-color/-/normalize-color-2.0.0.tgz", - "integrity": "sha512-Wip/xsc5lw8vsBlmY2MO/gFLp3MvuZ2baBZjDeTjjndMgM0h5sxz7AZR62RDPGgstp8Np7JzjvVqVT7tpFZqsw==", - "dev": true, - "peer": true - }, - "node_modules/@react-native/polyfills": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@react-native/polyfills/-/polyfills-2.0.0.tgz", - "integrity": "sha512-K0aGNn1TjalKj+65D7ycc1//H9roAQ51GJVk5ZJQFb2teECGmzd86bYDC0aYdbRf7gtovescq4Zt6FR0tgXiHQ==", - "dev": true, - "peer": true - }, "node_modules/@servie/events": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@servie/events/-/events-1.0.0.tgz", "integrity": "sha512-sBSO19KzdrJCM3gdx6eIxV8M9Gxfgg6iDQmH5TIAGaUu+X9VDdsINXJOnoiZ1Kx3TrHdH4bt5UVglkjsEGBcvw==", "dev": true }, - "node_modules/@sideway/address": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.4.tgz", - "integrity": "sha512-7vwq+rOHVWjyXxVlR76Agnvhy8I9rpzjosTESvmhNeXOXdZZB15Fl+TI9x1SiHZH5Jv2wTGduSxFDIaq0m3DUw==", - "dev": true, - "peer": true, - "dependencies": { - "@hapi/hoek": "^9.0.0" - } - }, - "node_modules/@sideway/formula": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.0.tgz", - "integrity": "sha512-vHe7wZ4NOXVfkoRb8T5otiENVlT7a3IAiw7H5M2+GO+9CDgcVUUsX1zalAztCmwyOr2RUTGJdgB+ZvSVqmdHmg==", - "dev": true, - "peer": true - }, - "node_modules/@sideway/pinpoint": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@sideway/pinpoint/-/pinpoint-2.0.0.tgz", - "integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==", - "dev": true, - "peer": true - }, "node_modules/@testing-library/cypress": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/@testing-library/cypress/-/cypress-9.0.0.tgz", - "integrity": "sha512-c1XiCGeHGGTWn0LAU12sFUfoX3qfId5gcSE2yHode+vsyHDWraxDPALjVnHd4/Fa3j4KBcc5k++Ccy6A9qnkMA==", + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@testing-library/cypress/-/cypress-10.0.1.tgz", + "integrity": "sha512-e8uswjTZIBhaIXjzEcrQQ8nHRWHgZH7XBxKuIWxZ/T7FxfWhCR48nFhUX5nfPizjVOKSThEfOSv67jquc1ASkw==", "dev": true, "dependencies": { "@babel/runtime": "^7.14.6", - "@testing-library/dom": "^8.1.0" + "@testing-library/dom": "^9.0.0" }, "engines": { "node": ">=12", "npm": ">=6" }, "peerDependencies": { - "cypress": "^12.0.0" + "cypress": "^12.0.0 || ^13.0.0" } }, "node_modules/@testing-library/dom": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-8.16.0.tgz", - "integrity": "sha512-uxF4zmnLHHDlmW4l+0WDjcgLVwCvH+OVLpD8Dfp+Bjfz85prwxWGbwXgJdLtkgjD0qfOzkJF9SmA6YZPsMYX4w==", + "version": "9.3.3", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-9.3.3.tgz", + "integrity": "sha512-fB0R+fa3AUqbLHWyxXa2kGVtf1Fe1ZZFr0Zp6AIbIAzXb2mKbEXl+PCQNUOaq5lbTab5tfctfXRNsWXxa2f7Aw==", "dev": true, "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", - "@types/aria-query": "^4.2.0", - "aria-query": "^5.0.0", + "@types/aria-query": "^5.0.1", + "aria-query": "5.1.3", "chalk": "^4.1.0", "dom-accessibility-api": "^0.5.9", - "lz-string": "^1.4.4", + "lz-string": "^1.5.0", "pretty-format": "^27.0.2" }, "engines": { - "node": ">=12" + "node": ">=14" + } + }, + "node_modules/@testing-library/dom/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, "node_modules/@testing-library/dom/node_modules/pretty-format": { @@ -3758,34 +1014,22 @@ "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" } }, - "node_modules/@testing-library/dom/node_modules/pretty-format/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, "node_modules/@types/aria-query": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-4.2.2.tgz", - "integrity": "sha512-HnYpAE1Y6kRyKM/XkEuiRQhTHvkzMBurTHnpFLYLBGPIylZNPs9jJcuOOYWxPLJCSEtmZT0Y8rHDokKN7rRTig==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.1.tgz", + "integrity": "sha512-XTIieEY+gvJ39ChLcB4If5zHtPxt3Syj5rgZR+e1ctpmK8NjPf0zFqsz4JpLJT0xla9GFDKjy8Cpu331nrmE1Q==", "dev": true }, "node_modules/@types/async": { - "version": "3.2.20", - "resolved": "https://registry.npmjs.org/@types/async/-/async-3.2.20.tgz", - "integrity": "sha512-6jSBQQugzyX1aWto0CbvOnmxrU9tMoXfA9gc4IrLEtvr3dTwSg5GLGoWiZnGLI6UG/kqpB3JOQKQrqnhUWGKQA==", + "version": "3.2.21", + "resolved": "https://registry.npmjs.org/@types/async/-/async-3.2.21.tgz", + "integrity": "sha512-msYM0OYxzkwpiDJZZDo7XsYuFIpiAOh+o3hygudSpSzeW+R0DAEKZvdQriHYT95m9CvscLDg5s0Mxlrlhoy2qw==", "dev": true }, "node_modules/@types/authenticator": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@types/authenticator/-/authenticator-1.1.1.tgz", - "integrity": "sha512-yEIqr179ISDa7XM5g/+aKGi+lXSVkheR8pWtVhWAM1G+v1aLari92SkTUIZPCI1Lpk+PhXGWdCtehm99IqoOFg==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@types/authenticator/-/authenticator-1.1.2.tgz", + "integrity": "sha512-0GksFKLfPTubVYNpwQbQ/TTumK6YFA+yxh4KLSe7QrnY/0bI27P5YJwKQDLF4NPOtJm+ywKg+ptwP8Za4GLtxQ==", "dev": true }, "node_modules/@types/body-parser": { @@ -3837,9 +1081,9 @@ "peer": true }, "node_modules/@types/express": { - "version": "4.17.17", - "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.17.tgz", - "integrity": "sha512-Q4FmmuLGBG58btUnfS1c1r/NQdlp3DMfGDGig8WhfpA2YRUtEkxAjkZb0yvplJGYdF1fsQ81iMDcH24sSCNC/Q==", + "version": "4.17.18", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.18.tgz", + "integrity": "sha512-Sxv8BSLLgsBYmcnGdGjjEjqET2U+AKAdCRODmMiq02FgjwuV75Ut85DRpvFjyw/Mk0vgUOliGRU0UUmuuZHByQ==", "dev": true, "dependencies": { "@types/body-parser": "*", @@ -3861,9 +1105,9 @@ } }, "node_modules/@types/fs-extra": { - "version": "11.0.1", - "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-11.0.1.tgz", - "integrity": "sha512-MxObHvNl4A69ofaTRU8DFqvgzzv8s9yRtaPPm5gud9HDNvpB3GPQFvNuTWAI59B9huVGV5jXYJwbCsmBsOGYWA==", + "version": "11.0.2", + "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-11.0.2.tgz", + "integrity": "sha512-c0hrgAOVYr21EX8J0jBMXGLMgJqVf/v6yxi0dLaJboW9aQPh16Id+z6w2Tx1hm+piJOLv8xPfVKZCLfjPw/IMQ==", "dev": true, "dependencies": { "@types/jsonfile": "*", @@ -3880,47 +1124,10 @@ "@types/node": "*" } }, - "node_modules/@types/graceful-fs": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.5.tgz", - "integrity": "sha512-anKkLmZZ+xm4p8JWBf4hElkM4XR+EZeA2M9BAkkTldmcyDY4mbdIJnRghDJH3Ov5ooY7/UAoENtmdMSkaAd7Cw==", - "dev": true, - "peer": true, - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/istanbul-lib-coverage": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz", - "integrity": "sha512-z/QT1XN4K4KYuslS23k62yDIDLwLFkzxOuMplDtObz0+y7VqJCaO2o+SPwHCvLFZh7xazvvoor2tA/hPz9ee7g==", - "dev": true, - "peer": true - }, - "node_modules/@types/istanbul-lib-report": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz", - "integrity": "sha512-plGgXAPfVKFoYfa9NpYDAkseG+g6Jr294RqeqcqDixSbU34MZVJRi/P+7Y8GDpzkEwLaGZZOpKIEmeVZNtKsrg==", - "dev": true, - "peer": true, - "dependencies": { - "@types/istanbul-lib-coverage": "*" - } - }, - "node_modules/@types/istanbul-reports": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.1.tgz", - "integrity": "sha512-c3mAZEuK0lvBp8tmuL74XRKn1+y2dcwOUpH7x4WrF6gk1GIgiluDRgMYQtw2OFcBvAJWlt6ASU3tSqxp0Uu0Aw==", - "dev": true, - "peer": true, - "dependencies": { - "@types/istanbul-lib-report": "*" - } - }, "node_modules/@types/json-schema": { - "version": "7.0.11", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz", - "integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==", + "version": "7.0.13", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.13.tgz", + "integrity": "sha512-RbSSoHliUbnXj3ny0CNFOoxrIDV6SUGyStHsvDqosw6CkdPV8TtWGlfecuK4ToyMEAql6pzNxgCFKanovUzlgQ==", "dev": true }, "node_modules/@types/json5": { @@ -3939,9 +1146,9 @@ } }, "node_modules/@types/lodash": { - "version": "4.14.194", - "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.194.tgz", - "integrity": "sha512-r22s9tAS7imvBt2lyHC9B8AGwWnXaYb1tY09oyLkXDs4vArpYJzw09nj8MLx5VfciBPGIb+ZwG0ssYnEPJxn/g==", + "version": "4.14.199", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.199.tgz", + "integrity": "sha512-Vrjz5N5Ia4SEzWWgIVwnHNEnb1UE1XMkvY5DGXrAeOGE9imk0hgTHh5GyDjLDJi9OTCn9oo9dXH1uToK1VRfrg==", "dev": true }, "node_modules/@types/lodash.intersection": { @@ -3978,9 +1185,9 @@ "dev": true }, "node_modules/@types/mime-types": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@types/mime-types/-/mime-types-2.1.1.tgz", - "integrity": "sha512-vXOTGVSLR2jMw440moWTC7H19iUyLtP3Z1YTj7cSsubOICinjMxFeb/V57v9QdyyPGbbWolUFSSmSiRSn94tFw==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@types/mime-types/-/mime-types-2.1.2.tgz", + "integrity": "sha512-q9QGHMGCiBJCHEvd4ZLdasdqXv570agPsUW0CeIm/B8DzhxsYMerD0l3IlI+EQ1A2RWHY2mmM9x1YIuuWxisCg==", "dev": true }, "node_modules/@types/minimatch": { @@ -3996,24 +1203,24 @@ "dev": true }, "node_modules/@types/mochawesome": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/@types/mochawesome/-/mochawesome-6.2.1.tgz", - "integrity": "sha512-AhQdBkT/CBdx3sI9ATeljqa5uJ3dGNKEJnsgzw9IkPeg9d9Lzxsz1eKFnenxq1qQojIW0XkIsCgdheRRXP2SQA==", + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/@types/mochawesome/-/mochawesome-6.2.2.tgz", + "integrity": "sha512-UqE7Mbw6Z2Nk3gXjbAM43p+Z3LpfH4624JtihzsR17sRlZo1UFBT0/zznE82wXvnI4gB/xWz0BxfwQpP0qB3Uw==", "dev": true, "dependencies": { "@types/mocha": "*" } }, "node_modules/@types/node": { - "version": "14.17.5", - "resolved": "https://registry.npmjs.org/@types/node/-/node-14.17.5.tgz", - "integrity": "sha512-bjqH2cX/O33jXT/UmReo2pM7DIJREPMnarixbQ57DOOzzFaI6D2+IcwaJQaJpv0M1E9TIhPCYVxrkcityLjlqA==", + "version": "18.17.18", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.17.18.tgz", + "integrity": "sha512-/4QOuy3ZpV7Ya1GTRz5CYSz3DgkKpyUptXuQ5PPce7uuyJAOR7r9FhkmxJfvcNUXyklbC63a+YvB3jxy7s9ngw==", "dev": true }, "node_modules/@types/pdf-parse": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@types/pdf-parse/-/pdf-parse-1.1.1.tgz", - "integrity": "sha512-lDBKAslCwvfK2uvS1Uk+UCpGvw+JRy5vnBFANPKFSY92n/iEnunXi0KVBjPJXhsM4jtdcPnS7tuZ0zjA9x6piQ==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@types/pdf-parse/-/pdf-parse-1.1.2.tgz", + "integrity": "sha512-4ftxqGUcYYjOmh6aTHvFZk9Y7N4j1+UCqBv1hRz+21zXnr08x9ZZeYovJUjyK7r7TthuLX+UND9Q42KFpbvF0g==", "dev": true }, "node_modules/@types/qs": { @@ -4029,18 +1236,18 @@ "dev": true }, "node_modules/@types/recursive-readdir": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/@types/recursive-readdir/-/recursive-readdir-2.2.1.tgz", - "integrity": "sha512-Xd+Ptc4/F2ueInqy5yK2FI5FxtwwbX2+VZpcg+9oYsFJVen8qQKGapCr+Bi5wQtHU1cTXT8s+07lo/nKPgu8Gg==", + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/@types/recursive-readdir/-/recursive-readdir-2.2.2.tgz", + "integrity": "sha512-pYLuc3EaFA3SwYfZRC6cSV9TfL+0tLi9Otu2djGavsNhmrEna1yG1EkFOZW7SKJr/nIuDNk4mM6kMMpNLntIhw==", "dev": true, "dependencies": { "@types/node": "*" } }, "node_modules/@types/semver": { - "version": "7.3.13", - "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.3.13.tgz", - "integrity": "sha512-21cFJr9z3g5dW8B0CVI9g2O9beqaThGQ6ZFBqHfwhzLDKUxaqTIy3vnfah/UPkfOiF2pLq+tGz+W8RyCskuslw==", + "version": "7.5.3", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.3.tgz", + "integrity": "sha512-OxepLK9EuNEIPxWNME+C6WwbRAOOI2o2BaQEGzz5Lu2e4Z5eDnEo+/aVEDMIXywoJitJ7xWd641wrGLZdtwRyw==", "dev": true }, "node_modules/@types/send": { @@ -4064,9 +1271,9 @@ } }, "node_modules/@types/shelljs": { - "version": "0.8.12", - "resolved": "https://registry.npmjs.org/@types/shelljs/-/shelljs-0.8.12.tgz", - "integrity": "sha512-ZA8U81/gldY+rR5zl/7HSHrG2KDfEb3lzG6uCUDhW1DTQE9yC/VBQ45fXnXq8f3CgInfhZmjtdu/WOUlrXRQUg==", + "version": "0.8.13", + "resolved": "https://registry.npmjs.org/@types/shelljs/-/shelljs-0.8.13.tgz", + "integrity": "sha512-++uMLOQSLlse1kCfEOwhgmHuaABZwinkylmUKCpvcEGZUov3TtM+gJZloSkW/W+9pEAEg/VBOwiSR05oqJsa5A==", "dev": true, "dependencies": { "@types/glob": "~7.2.0", @@ -4092,28 +1299,11 @@ "dev": true }, "node_modules/@types/uuid": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.1.tgz", - "integrity": "sha512-rFT3ak0/2trgvp4yYZo5iKFEPsET7vKydKF+VRCxlQ9bpheehyAJH89dAkaLEq/j/RZXJIqcgsmPJKUP1Z28HA==", + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.4.tgz", + "integrity": "sha512-zAuJWQflfx6dYJM62vna+Sn5aeSWhh3OB+wfUEACNcqUSc0AGc5JKl+ycL1vrH7frGTXhJchYjE1Hak8L819dA==", "dev": true }, - "node_modules/@types/yargs": { - "version": "16.0.4", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-16.0.4.tgz", - "integrity": "sha512-T8Yc9wt/5LbJyCaLiHPReJa0kApcIgJ7Bn735GjItUfh08Z1pJvu8QZqb9s+mMvKV6WUQRV7K2R46YbjMXTTJw==", - "dev": true, - "peer": true, - "dependencies": { - "@types/yargs-parser": "*" - } - }, - "node_modules/@types/yargs-parser": { - "version": "21.0.0", - "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.0.tgz", - "integrity": "sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA==", - "dev": true, - "peer": true - }, "node_modules/@types/yauzl": { "version": "2.9.2", "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.9.2.tgz", @@ -4125,32 +1315,33 @@ } }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "5.59.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.59.2.tgz", - "integrity": "sha512-yVrXupeHjRxLDcPKL10sGQ/QlVrA8J5IYOEWVqk0lJaSZP7X5DfnP7Ns3cc74/blmbipQ1htFNVGsHX6wsYm0A==", + "version": "6.7.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.7.4.tgz", + "integrity": "sha512-DAbgDXwtX+pDkAHwiGhqP3zWUGpW49B7eqmgpPtg+BKJXwdct79ut9+ifqOFPJGClGKSHXn2PTBatCnldJRUoA==", "dev": true, "dependencies": { - "@eslint-community/regexpp": "^4.4.0", - "@typescript-eslint/scope-manager": "5.59.2", - "@typescript-eslint/type-utils": "5.59.2", - "@typescript-eslint/utils": "5.59.2", + "@eslint-community/regexpp": "^4.5.1", + "@typescript-eslint/scope-manager": "6.7.4", + "@typescript-eslint/type-utils": "6.7.4", + "@typescript-eslint/utils": "6.7.4", + "@typescript-eslint/visitor-keys": "6.7.4", "debug": "^4.3.4", - "grapheme-splitter": "^1.0.4", - "ignore": "^5.2.0", - "natural-compare-lite": "^1.4.0", - "semver": "^7.3.7", - "tsutils": "^3.21.0" + "graphemer": "^1.4.0", + "ignore": "^5.2.4", + "natural-compare": "^1.4.0", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^16.0.0 || >=18.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^5.0.0", - "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + "@typescript-eslint/parser": "^6.0.0 || ^6.0.0-alpha", + "eslint": "^7.0.0 || ^8.0.0" }, "peerDependenciesMeta": { "typescript": { @@ -4159,9 +1350,9 @@ } }, "node_modules/@typescript-eslint/eslint-plugin/node_modules/semver": { - "version": "7.3.8", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", - "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", "dev": true, "dependencies": { "lru-cache": "^6.0.0" @@ -4174,25 +1365,26 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "5.59.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.59.2.tgz", - "integrity": "sha512-uq0sKyw6ao1iFOZZGk9F8Nro/8+gfB5ezl1cA06SrqbgJAt0SRoFhb9pXaHvkrxUpZaoLxt8KlovHNk8Gp6/HQ==", + "version": "6.7.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.7.4.tgz", + "integrity": "sha512-I5zVZFY+cw4IMZUeNCU7Sh2PO5O57F7Lr0uyhgCJmhN/BuTlnc55KxPonR4+EM3GBdfiCyGZye6DgMjtubQkmA==", "dev": true, "dependencies": { - "@typescript-eslint/scope-manager": "5.59.2", - "@typescript-eslint/types": "5.59.2", - "@typescript-eslint/typescript-estree": "5.59.2", + "@typescript-eslint/scope-manager": "6.7.4", + "@typescript-eslint/types": "6.7.4", + "@typescript-eslint/typescript-estree": "6.7.4", + "@typescript-eslint/visitor-keys": "6.7.4", "debug": "^4.3.4" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^16.0.0 || >=18.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + "eslint": "^7.0.0 || ^8.0.0" }, "peerDependenciesMeta": { "typescript": { @@ -4201,16 +1393,16 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "5.59.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.59.2.tgz", - "integrity": "sha512-dB1v7ROySwQWKqQ8rEWcdbTsFjh2G0vn8KUyvTXdPoyzSL6lLGkiXEV5CvpJsEe9xIdKV+8Zqb7wif2issoOFA==", + "version": "6.7.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.7.4.tgz", + "integrity": "sha512-SdGqSLUPTXAXi7c3Ob7peAGVnmMoGzZ361VswK2Mqf8UOYcODiYvs8rs5ILqEdfvX1lE7wEZbLyELCW+Yrql1A==", "dev": true, "dependencies": { - "@typescript-eslint/types": "5.59.2", - "@typescript-eslint/visitor-keys": "5.59.2" + "@typescript-eslint/types": "6.7.4", + "@typescript-eslint/visitor-keys": "6.7.4" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^16.0.0 || >=18.0.0" }, "funding": { "type": "opencollective", @@ -4218,25 +1410,25 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "5.59.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.59.2.tgz", - "integrity": "sha512-b1LS2phBOsEy/T381bxkkywfQXkV1dWda/z0PhnIy3bC5+rQWQDS7fk9CSpcXBccPY27Z6vBEuaPBCKCgYezyQ==", + "version": "6.7.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.7.4.tgz", + "integrity": "sha512-n+g3zi1QzpcAdHFP9KQF+rEFxMb2KxtnJGID3teA/nxKHOVi3ylKovaqEzGBbVY2pBttU6z85gp0D00ufLzViQ==", "dev": true, "dependencies": { - "@typescript-eslint/typescript-estree": "5.59.2", - "@typescript-eslint/utils": "5.59.2", + "@typescript-eslint/typescript-estree": "6.7.4", + "@typescript-eslint/utils": "6.7.4", "debug": "^4.3.4", - "tsutils": "^3.21.0" + "ts-api-utils": "^1.0.1" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^16.0.0 || >=18.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "*" + "eslint": "^7.0.0 || ^8.0.0" }, "peerDependenciesMeta": { "typescript": { @@ -4245,12 +1437,12 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "5.59.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.59.2.tgz", - "integrity": "sha512-LbJ/HqoVs2XTGq5shkiKaNTuVv5tTejdHgfdjqRUGdYhjW1crm/M7og2jhVskMt8/4wS3T1+PfFvL1K3wqYj4w==", + "version": "6.7.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.7.4.tgz", + "integrity": "sha512-o9XWK2FLW6eSS/0r/tgjAGsYasLAnOWg7hvZ/dGYSSNjCh+49k5ocPN8OmG5aZcSJ8pclSOyVKP2x03Sj+RrCA==", "dev": true, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^16.0.0 || >=18.0.0" }, "funding": { "type": "opencollective", @@ -4258,21 +1450,21 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "5.59.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.59.2.tgz", - "integrity": "sha512-+j4SmbwVmZsQ9jEyBMgpuBD0rKwi9RxRpjX71Brr73RsYnEr3Lt5QZ624Bxphp8HUkSKfqGnPJp1kA5nl0Sh7Q==", + "version": "6.7.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.7.4.tgz", + "integrity": "sha512-ty8b5qHKatlNYd9vmpHooQz3Vki3gG+3PchmtsA4TgrZBKWHNjWfkQid7K7xQogBqqc7/BhGazxMD5vr6Ha+iQ==", "dev": true, "dependencies": { - "@typescript-eslint/types": "5.59.2", - "@typescript-eslint/visitor-keys": "5.59.2", + "@typescript-eslint/types": "6.7.4", + "@typescript-eslint/visitor-keys": "6.7.4", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", - "semver": "^7.3.7", - "tsutils": "^3.21.0" + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^16.0.0 || >=18.0.0" }, "funding": { "type": "opencollective", @@ -4285,9 +1477,9 @@ } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { - "version": "7.5.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.0.tgz", - "integrity": "sha512-+XC0AD/R7Q2mPSRuy2Id0+CGTZ98+8f+KvwirxOKIEyid+XSx6HbC63p+O4IndTHuX5Z+JxQ0TghCkO5Cg/2HA==", + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", "dev": true, "dependencies": { "lru-cache": "^6.0.0" @@ -4300,35 +1492,34 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "5.59.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.59.2.tgz", - "integrity": "sha512-kSuF6/77TZzyGPhGO4uVp+f0SBoYxCDf+lW3GKhtKru/L8k/Hd7NFQxyWUeY7Z/KGB2C6Fe3yf2vVi4V9TsCSQ==", + "version": "6.7.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.7.4.tgz", + "integrity": "sha512-PRQAs+HUn85Qdk+khAxsVV+oULy3VkbH3hQ8hxLRJXWBEd7iI+GbQxH5SEUSH7kbEoTp6oT1bOwyga24ELALTA==", "dev": true, "dependencies": { - "@eslint-community/eslint-utils": "^4.2.0", - "@types/json-schema": "^7.0.9", - "@types/semver": "^7.3.12", - "@typescript-eslint/scope-manager": "5.59.2", - "@typescript-eslint/types": "5.59.2", - "@typescript-eslint/typescript-estree": "5.59.2", - "eslint-scope": "^5.1.1", - "semver": "^7.3.7" + "@eslint-community/eslint-utils": "^4.4.0", + "@types/json-schema": "^7.0.12", + "@types/semver": "^7.5.0", + "@typescript-eslint/scope-manager": "6.7.4", + "@typescript-eslint/types": "6.7.4", + "@typescript-eslint/typescript-estree": "6.7.4", + "semver": "^7.5.4" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^16.0.0 || >=18.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + "eslint": "^7.0.0 || ^8.0.0" } }, "node_modules/@typescript-eslint/utils/node_modules/semver": { - "version": "7.5.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.0.tgz", - "integrity": "sha512-+XC0AD/R7Q2mPSRuy2Id0+CGTZ98+8f+KvwirxOKIEyid+XSx6HbC63p+O4IndTHuX5Z+JxQ0TghCkO5Cg/2HA==", + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", "dev": true, "dependencies": { "lru-cache": "^6.0.0" @@ -4341,16 +1532,16 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "5.59.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.59.2.tgz", - "integrity": "sha512-EEpsO8m3RASrKAHI9jpavNv9NlEUebV4qmF1OWxSTtKSFBpC1NCmWazDQHFivRf0O1DV11BA645yrLEVQ0/Lig==", + "version": "6.7.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.7.4.tgz", + "integrity": "sha512-pOW37DUhlTZbvph50x5zZCkFn3xzwkGtNoJHzIM3svpiSkJzwOYr/kVBaXmf+RAQiUDs1AHEZVNPg6UJCJpwRA==", "dev": true, "dependencies": { - "@typescript-eslint/types": "5.59.2", - "eslint-visitor-keys": "^3.3.0" + "@typescript-eslint/types": "6.7.4", + "eslint-visitor-keys": "^3.4.1" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^16.0.0 || >=18.0.0" }, "funding": { "type": "opencollective", @@ -4532,31 +1723,6 @@ "dev": true, "peer": true }, - "node_modules/@yarnpkg/lockfile": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz", - "integrity": "sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ==" - }, - "node_modules/abort-controller": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", - "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", - "dev": true, - "peer": true, - "dependencies": { - "event-target-shim": "^5.0.0" - }, - "engines": { - "node": ">=6.5" - } - }, - "node_modules/absolute-path": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/absolute-path/-/absolute-path-0.0.0.tgz", - "integrity": "sha512-HQiug4c+/s3WOvEnDRxXVmNtSG5s2gJM9r19BTcqjp7BWcE48PB+Y2G6jE65kqI0LpsQeMZygt/b60Gi4KxGyA==", - "dev": true, - "peer": true - }, "node_modules/accepts": { "version": "1.3.8", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", @@ -4571,9 +1737,9 @@ } }, "node_modules/acorn": { - "version": "8.8.2", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz", - "integrity": "sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==", + "version": "8.10.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", + "integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==", "dev": true, "bin": { "acorn": "bin/acorn" @@ -4640,13 +1806,6 @@ "platform": "1.3.3" } }, - "node_modules/anser": { - "version": "1.4.10", - "resolved": "https://registry.npmjs.org/anser/-/anser-1.4.10.tgz", - "integrity": "sha512-hCv9AqTQ8ycjpSd3upOJd7vFwW1JaoYQ7tpham03GJ1ca8/65rqn0RpaWpItOAd6ylW9wAw6luXYPJIyPFVOww==", - "dev": true, - "peer": true - }, "node_modules/ansi-colors": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", @@ -4671,83 +1830,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/ansi-fragments": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/ansi-fragments/-/ansi-fragments-0.2.1.tgz", - "integrity": "sha512-DykbNHxuXQwUDRv5ibc2b0x7uw7wmwOGLBUd5RmaQ5z8Lhx19vwvKV+FAsM5rEA6dEcHxX+/Ad5s9eF2k2bB+w==", - "dev": true, - "peer": true, - "dependencies": { - "colorette": "^1.0.7", - "slice-ansi": "^2.0.0", - "strip-ansi": "^5.0.0" - } - }, - "node_modules/ansi-fragments/node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "peer": true, - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/ansi-fragments/node_modules/astral-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-1.0.0.tgz", - "integrity": "sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==", - "dev": true, - "peer": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/ansi-fragments/node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "peer": true, - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/ansi-fragments/node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true, - "peer": true - }, - "node_modules/ansi-fragments/node_modules/is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w==", - "dev": true, - "peer": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/ansi-fragments/node_modules/slice-ansi": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-2.1.0.tgz", - "integrity": "sha512-Qu+VC3EwYLldKa1fCxuuvULvSJOKEgk9pi8dZeCVK7TqBfUNTH4sFkk4joj8afVSfAYgJoSOetjx9QWOJ5mYoQ==", - "dev": true, - "peer": true, - "dependencies": { - "ansi-styles": "^3.2.0", - "astral-regex": "^1.0.0", - "is-fullwidth-code-point": "^2.0.0" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", @@ -4761,6 +1843,7 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, "dependencies": { "color-convert": "^2.0.1" }, @@ -4784,13 +1867,6 @@ "node": ">= 8" } }, - "node_modules/appdirsjs": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/appdirsjs/-/appdirsjs-1.2.7.tgz", - "integrity": "sha512-Quji6+8kLBC3NnBeo14nPDq0+2jUs5s3/xEye+udFHumHhRk4M7aAMXp/PBJqkKYGuuyR9M/6Dq7d2AViiGmhw==", - "dev": true, - "peer": true - }, "node_modules/arch": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/arch/-/arch-2.2.0.tgz", @@ -4826,36 +1902,6 @@ "deep-equal": "^2.0.5" } }, - "node_modules/arr-diff": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", - "integrity": "sha512-YVIQ82gZPGBebQV/a8dar4AitzCQs0jjXwMPZllpXMaGjXPYVUawSxQrRsjhjupyVxEvbHgUmIhKVlND+j02kA==", - "dev": true, - "peer": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/arr-flatten": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz", - "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==", - "dev": true, - "peer": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/arr-union": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", - "integrity": "sha512-sKpyeERZ02v1FeCZT8lrfJq5u6goHCtpTAzPwJYe7c8SPFOboNjNg1vz2L4VTn9T4PQxEx13TbXLmYUcS6Ug7Q==", - "dev": true, - "peer": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/array-buffer-byte-length": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.0.tgz", @@ -4869,12 +1915,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/array-find": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/array-find/-/array-find-1.0.0.tgz", - "integrity": "sha512-kO/vVCacW9mnpn3WPWbTVlEnOabK2L7LWi2HViURtCM46y1zb6I8UMjx4LgbiqadTgHnLInUronwn3ampNTJtQ==", - "dev": true - }, "node_modules/array-flatten": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", @@ -4909,14 +1949,38 @@ "node": ">=8" } }, - "node_modules/array-unique": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", - "integrity": "sha512-SleRWjh9JUud2wH1hPs9rZBZ33H6T9HOiL0uwGnGx9FpE6wKGyfWugmbkEOIs6qWrZhg0LWeLziLrEwQJhs5mQ==", + "node_modules/array.prototype.find": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/array.prototype.find/-/array.prototype.find-2.2.2.tgz", + "integrity": "sha512-DRumkfW97iZGOfn+lIXbkVrXL04sfYKX+EfOodo8XboR5sxPDVvOjZTF/rysusa9lmhmSOeD6Vp6RKQP+eP4Tg==", "dev": true, - "peer": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "es-shim-unscopables": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.findlastindex": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.3.tgz", + "integrity": "sha512-LzLoiOMAxvy+Gd3BAq3B7VeIgPdo+Q8hthvKtXybMvRV0jrXfJM/t8mw7nNlpEcVlVUnCnM2KSX4XU5HmpodOA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "es-shim-unscopables": "^1.0.0", + "get-intrinsic": "^1.2.1" + }, "engines": { - "node": ">=0.10.0" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/array.prototype.flat": { @@ -4968,12 +2032,26 @@ "get-intrinsic": "^1.1.3" } }, - "node_modules/asap": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", - "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", + "node_modules/arraybuffer.prototype.slice": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.2.tgz", + "integrity": "sha512-yMBKppFur/fbHu9/6USUe03bZ4knMYiwFBcyiaXB8Go0qNehwX6inYPzK9U0NeQvGxKthcmHcaR8P5MStSRBAw==", "dev": true, - "peer": true + "dependencies": { + "array-buffer-byte-length": "^1.0.0", + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "get-intrinsic": "^1.2.1", + "is-array-buffer": "^3.0.2", + "is-shared-array-buffer": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, "node_modules/asn1": { "version": "0.2.6", @@ -5002,29 +2080,6 @@ "node": "*" } }, - "node_modules/assign-symbols": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", - "integrity": "sha512-Q+JC7Whu8HhmTdBph/Tq59IoRtoy6KAm5zzPv00WdujX82lbAL8K7WVjne7vdCsAmbF4AYaDOPyO3k0kl8qIrw==", - "dev": true, - "peer": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/ast-types": { - "version": "0.14.2", - "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.14.2.tgz", - "integrity": "sha512-O0yuUDnZeQDL+ncNGlJ78BiO4jnYI3bvMsD5prT0/nsgijG/LpNBIr63gTjVTNsiGkgQhiyCShTgxt8oXOrklA==", - "dev": true, - "peer": true, - "dependencies": { - "tslib": "^2.0.1" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/ast-types-flow": { "version": "0.0.7", "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.7.tgz", @@ -5046,12 +2101,14 @@ "integrity": "sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==", "dev": true }, - "node_modules/async-limiter": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz", - "integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==", + "node_modules/asynciterator.prototype": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/asynciterator.prototype/-/asynciterator.prototype-1.0.0.tgz", + "integrity": "sha512-wwHYEIS0Q80f5mosx3L/dfG5t5rjEa9Ft51GTaNt862EnpyGHpgz2RkZvLPp1oF5TnAiTohkEKVEu8pQPJI7Vg==", "dev": true, - "peer": true + "dependencies": { + "has-symbols": "^1.0.3" + } }, "node_modules/asynckit": { "version": "0.4.0", @@ -5063,23 +2120,11 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", + "dev": true, "engines": { "node": ">= 4.0.0" } }, - "node_modules/atob": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", - "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==", - "dev": true, - "peer": true, - "bin": { - "atob": "bin/atob.js" - }, - "engines": { - "node": ">= 4.5.0" - } - }, "node_modules/authenticator": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/authenticator/-/authenticator-1.1.5.tgz", @@ -5121,9 +2166,9 @@ } }, "node_modules/aws-sdk": { - "version": "2.1371.0", - "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.1371.0.tgz", - "integrity": "sha512-dIts8xcmi7MFN1THOOcjiyqdvTqSVnsA4iIIerfyvpeYNobIi9pqBjCX9m8tfJ0pXQnymd4kp0xK9Y+i5dFfEQ==", + "version": "2.1468.0", + "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.1468.0.tgz", + "integrity": "sha512-1DAa0UA779M0VyKKPjTPDtox4KOyFrHnGDLbJrDKzqylC+O0DMR4kh9Oy2vfErhwFwsiPUvWgkknddVS+igVGA==", "dev": true, "dependencies": { "buffer": "4.9.2", @@ -5188,9 +2233,9 @@ } }, "node_modules/axios": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.4.0.tgz", - "integrity": "sha512-S4XCWMEmzvo64T9GfvQDOXgYRDJ/wsSZc7Jvdgx5u1sd0JwsuPLqb3SYmusag+edF6ziyMensPVqLTSc1PiSEA==", + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.5.1.tgz", + "integrity": "sha512-Q28iYCWzNHjAm+yEAot5QaAMxhMghWLFVf7rRdwhUI+c2jix2DUXjAHXVi+s1ibs3mjPO/cCgbA++3BjD0vP/A==", "dev": true, "dependencies": { "follow-redirects": "^1.15.0", @@ -5199,9 +2244,9 @@ } }, "node_modules/axios-retry": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/axios-retry/-/axios-retry-3.4.0.tgz", - "integrity": "sha512-VdgaP+gHH4iQYCCNUWF2pcqeciVOdGrBBAYUfTY+wPcO5Ltvp/37MLFNCmJKo7Gj3SHvCSdL8ouI1qLYJN3liA==", + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/axios-retry/-/axios-retry-3.8.0.tgz", + "integrity": "sha512-CfIsQyWNc5/AE7x/UEReRUadiBmQeoBpSEC+4QyGLJMswTsP1tz0GW2YYPnE7w9+ESMef5zOgLDFpHynNyEZ1w==", "dev": true, "dependencies": { "@babel/runtime": "^7.15.4", @@ -5237,164 +2282,11 @@ "deep-equal": "^2.0.5" } }, - "node_modules/babel-core": { - "version": "7.0.0-bridge.0", - "resolved": "https://registry.npmjs.org/babel-core/-/babel-core-7.0.0-bridge.0.tgz", - "integrity": "sha512-poPX9mZH/5CSanm50Q+1toVci6pv5KSRv/5TWCwtzQS5XEwn40BcCrgIeMFWP9CKKIniKXNxoIOnOq4VVlGXhg==", - "dev": true, - "peer": true, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/babel-plugin-polyfill-corejs2": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.3.3.tgz", - "integrity": "sha512-8hOdmFYFSZhqg2C/JgLUQ+t52o5nirNwaWM2B9LWteozwIvM14VSwdsCAUET10qT+kmySAlseadmfeeSWFCy+Q==", - "dev": true, - "peer": true, - "dependencies": { - "@babel/compat-data": "^7.17.7", - "@babel/helper-define-polyfill-provider": "^0.3.3", - "semver": "^6.1.1" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/babel-plugin-polyfill-corejs3": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.5.2.tgz", - "integrity": "sha512-G3uJih0XWiID451fpeFaYGVuxHEjzKTHtc9uGFEjR6hHrvNzeS/PX+LLLcetJcytsB5m4j+K3o/EpXJNb/5IEQ==", - "dev": true, - "peer": true, - "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.3.1", - "core-js-compat": "^3.21.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/babel-plugin-polyfill-regenerator": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.3.1.tgz", - "integrity": "sha512-Y2B06tvgHYt1x0yz17jGkGeeMr5FeKUu+ASJ+N6nB5lQ8Dapfg42i0OVrf8PNGJ3zKL4A23snMi1IRwrqqND7A==", - "dev": true, - "peer": true, - "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.3.1" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/babel-plugin-syntax-trailing-function-commas": { - "version": "7.0.0-beta.0", - "resolved": "https://registry.npmjs.org/babel-plugin-syntax-trailing-function-commas/-/babel-plugin-syntax-trailing-function-commas-7.0.0-beta.0.tgz", - "integrity": "sha512-Xj9XuRuz3nTSbaTXWv3itLOcxyF4oPD8douBBmj7U9BBC6nEBYfyOJYQMf/8PJAFotC62UY5dFfIGEPr7WswzQ==", - "dev": true, - "peer": true - }, - "node_modules/babel-preset-fbjs": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/babel-preset-fbjs/-/babel-preset-fbjs-3.4.0.tgz", - "integrity": "sha512-9ywCsCvo1ojrw0b+XYk7aFvTH6D9064t0RIL1rtMf3nsa02Xw41MS7sZw216Im35xj/UY0PDBQsa1brUDDF1Ow==", - "dev": true, - "peer": true, - "dependencies": { - "@babel/plugin-proposal-class-properties": "^7.0.0", - "@babel/plugin-proposal-object-rest-spread": "^7.0.0", - "@babel/plugin-syntax-class-properties": "^7.0.0", - "@babel/plugin-syntax-flow": "^7.0.0", - "@babel/plugin-syntax-jsx": "^7.0.0", - "@babel/plugin-syntax-object-rest-spread": "^7.0.0", - "@babel/plugin-transform-arrow-functions": "^7.0.0", - "@babel/plugin-transform-block-scoped-functions": "^7.0.0", - "@babel/plugin-transform-block-scoping": "^7.0.0", - "@babel/plugin-transform-classes": "^7.0.0", - "@babel/plugin-transform-computed-properties": "^7.0.0", - "@babel/plugin-transform-destructuring": "^7.0.0", - "@babel/plugin-transform-flow-strip-types": "^7.0.0", - "@babel/plugin-transform-for-of": "^7.0.0", - "@babel/plugin-transform-function-name": "^7.0.0", - "@babel/plugin-transform-literals": "^7.0.0", - "@babel/plugin-transform-member-expression-literals": "^7.0.0", - "@babel/plugin-transform-modules-commonjs": "^7.0.0", - "@babel/plugin-transform-object-super": "^7.0.0", - "@babel/plugin-transform-parameters": "^7.0.0", - "@babel/plugin-transform-property-literals": "^7.0.0", - "@babel/plugin-transform-react-display-name": "^7.0.0", - "@babel/plugin-transform-react-jsx": "^7.0.0", - "@babel/plugin-transform-shorthand-properties": "^7.0.0", - "@babel/plugin-transform-spread": "^7.0.0", - "@babel/plugin-transform-template-literals": "^7.0.0", - "babel-plugin-syntax-trailing-function-commas": "^7.0.0-beta.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/babel-runtime": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz", - "integrity": "sha512-ITKNuq2wKlW1fJg9sSW52eepoYgZBggvOAHC0u/CYu/qxQ9EVzThCgR69BnSXLHjy2f7SY5zaQ4yt7H9ZVxY2g==", - "dev": true, - "dependencies": { - "core-js": "^2.4.0", - "regenerator-runtime": "^0.11.0" - } - }, - "node_modules/babel-runtime/node_modules/core-js": { - "version": "2.6.12", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.12.tgz", - "integrity": "sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ==", - "deprecated": "core-js@<3.23.3 is no longer maintained and not recommended for usage due to the number of issues. Because of the V8 engine whims, feature detection in old core-js versions could cause a slowdown up to 100x even if nothing is polyfilled. Some versions have web compatibility issues. Please, upgrade your dependencies to the actual version of core-js.", - "dev": true, - "hasInstallScript": true - }, - "node_modules/babel-runtime/node_modules/regenerator-runtime": { - "version": "0.11.1", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz", - "integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==", - "dev": true - }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" - }, - "node_modules/base": { - "version": "0.11.2", - "resolved": "https://registry.npmjs.org/base/-/base-0.11.2.tgz", - "integrity": "sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==", - "dev": true, - "peer": true, - "dependencies": { - "cache-base": "^1.0.1", - "class-utils": "^0.3.5", - "component-emitter": "^1.2.1", - "define-property": "^1.0.0", - "isobject": "^3.0.1", - "mixin-deep": "^1.2.0", - "pascalcase": "^0.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/base/node_modules/define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha512-cZTYKFWspt9jZsMscWo8sc/5lbPC9Q0N5nBLgb+Yd915iL3udB1uFgS3B8YCx66UVHq018DAVFoee7x+gxggeA==", - "dev": true, - "peer": true, - "dependencies": { - "is-descriptor": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true }, "node_modules/base64-js": { "version": "1.5.1", @@ -5443,58 +2335,6 @@ "node": ">=8" } }, - "node_modules/bl": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", - "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", - "dev": true, - "peer": true, - "dependencies": { - "buffer": "^5.5.0", - "inherits": "^2.0.4", - "readable-stream": "^3.4.0" - } - }, - "node_modules/bl/node_modules/buffer": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", - "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "peer": true, - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.1.13" - } - }, - "node_modules/bl/node_modules/readable-stream": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", - "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", - "dev": true, - "peer": true, - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/blob-util": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/blob-util/-/blob-util-2.0.2.tgz", @@ -5577,6 +2417,7 @@ "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -5586,6 +2427,7 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, "dependencies": { "fill-range": "^7.0.1" }, @@ -5628,16 +2470,6 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, - "node_modules/bser": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", - "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", - "dev": true, - "peer": true, - "dependencies": { - "node-int64": "^0.4.0" - } - }, "node_modules/buffer": { "version": "4.9.2", "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.2.tgz", @@ -5689,27 +2521,6 @@ "node": ">= 0.8" } }, - "node_modules/cache-base": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz", - "integrity": "sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==", - "dev": true, - "peer": true, - "dependencies": { - "collection-visit": "^1.0.0", - "component-emitter": "^1.2.1", - "get-value": "^2.0.6", - "has-value": "^1.0.0", - "isobject": "^3.0.1", - "set-value": "^2.0.0", - "to-object-path": "^0.3.0", - "union-value": "^1.0.0", - "unset-value": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/cachedir": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/cachedir/-/cachedir-2.3.0.tgz", @@ -5732,42 +2543,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/caller-callsite": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/caller-callsite/-/caller-callsite-2.0.0.tgz", - "integrity": "sha512-JuG3qI4QOftFsZyOn1qq87fq5grLIyk1JYd5lJmdA+fG7aQ9pA/i3JIJGcO3q0MrRcHlOt1U+ZeHW8Dq9axALQ==", - "dev": true, - "peer": true, - "dependencies": { - "callsites": "^2.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/caller-path": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/caller-path/-/caller-path-2.0.0.tgz", - "integrity": "sha512-MCL3sf6nCSXOwCTzvPKhN18TU7AHTvdtam8DAogxcrJ8Rjfbbg7Lgng64H9Iy+vUV6VGFClN/TyxBkAebLRR4A==", - "dev": true, - "peer": true, - "dependencies": { - "caller-callsite": "^2.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/callsites": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-2.0.0.tgz", - "integrity": "sha512-ksWePWBloaWPxJYQ8TL0JHvtci6G5QTKwQ95RcWAa/lzoAKuAOflGdAK92hpHXjkwb8zLxoLNUoNYZgVsaJzvQ==", - "dev": true, - "peer": true, - "engines": { - "node": ">=4" - } - }, "node_modules/camelcase": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.2.0.tgz", @@ -5804,18 +2579,18 @@ "dev": true }, "node_modules/chai": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.7.tgz", - "integrity": "sha512-HLnAzZ2iupm25PlN0xFreAlBA5zaBSv3og0DdeGA4Ar6h6rJ3A0rolRUKJhSF2V10GZKDgWF/VmAEsNWjCRB+A==", + "version": "4.3.10", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.10.tgz", + "integrity": "sha512-0UXG04VuVbruMUYbJ6JctvH0YnC/4q3/AkT18q4NaITo91CUm0liMS9VqzT9vZhVQ/1eqPanMWjBM+Juhfb/9g==", "dev": true, "dependencies": { "assertion-error": "^1.1.0", - "check-error": "^1.0.2", - "deep-eql": "^4.1.2", - "get-func-name": "^2.0.0", - "loupe": "^2.3.1", + "check-error": "^1.0.3", + "deep-eql": "^4.1.3", + "get-func-name": "^2.0.2", + "loupe": "^2.3.6", "pathval": "^1.1.1", - "type-detect": "^4.0.5" + "type-detect": "^4.0.8" }, "engines": { "node": ">=4" @@ -5825,6 +2600,7 @@ "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -5846,10 +2622,13 @@ } }, "node_modules/check-error": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", - "integrity": "sha1-V00xLt2Iu13YkS6Sht1sCu1KrII=", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz", + "integrity": "sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==", "dev": true, + "dependencies": { + "get-func-name": "^2.0.2" + }, "engines": { "node": "*" } @@ -5904,6 +2683,7 @@ "version": "3.8.0", "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.8.0.tgz", "integrity": "sha512-eXTggHWSooYhq49F2opQhuHWgzucfF2YgODK4e1566GQs5BIfP30B0oenwBJHfWxAs2fyPB1s7Mg949zLf61Yw==", + "dev": true, "funding": [ { "type": "github", @@ -5914,112 +2694,6 @@ "node": ">=8" } }, - "node_modules/class-utils": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz", - "integrity": "sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==", - "dev": true, - "peer": true, - "dependencies": { - "arr-union": "^3.1.0", - "define-property": "^0.2.5", - "isobject": "^3.0.0", - "static-extend": "^0.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/class-utils/node_modules/define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA==", - "dev": true, - "peer": true, - "dependencies": { - "is-descriptor": "^0.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/class-utils/node_modules/is-accessor-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", - "integrity": "sha512-e1BM1qnDbMRG3ll2U9dSK0UMHuWOs3pY3AtcFsmvwPtKL3MML/Q86i+GilLfvqEs4GW+ExB91tQ3Ig9noDIZ+A==", - "dev": true, - "peer": true, - "dependencies": { - "kind-of": "^3.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/class-utils/node_modules/is-accessor-descriptor/node_modules/kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", - "dev": true, - "peer": true, - "dependencies": { - "is-buffer": "^1.1.5" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/class-utils/node_modules/is-data-descriptor": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", - "integrity": "sha512-+w9D5ulSoBNlmw9OHn3U2v51SyoCd0he+bB3xMl62oijhrspxowjU+AIcDY0N3iEJbUEkB15IlMASQsxYigvXg==", - "dev": true, - "peer": true, - "dependencies": { - "kind-of": "^3.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/class-utils/node_modules/is-data-descriptor/node_modules/kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", - "dev": true, - "peer": true, - "dependencies": { - "is-buffer": "^1.1.5" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/class-utils/node_modules/is-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", - "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", - "dev": true, - "peer": true, - "dependencies": { - "is-accessor-descriptor": "^0.1.6", - "is-data-descriptor": "^0.1.4", - "kind-of": "^5.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/class-utils/node_modules/kind-of": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", - "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", - "dev": true, - "peer": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/clean-stack": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", @@ -6054,19 +2728,6 @@ "node": ">=8" } }, - "node_modules/cli-spinners": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.6.1.tgz", - "integrity": "sha512-x/5fWmGMnbKQAaNwN+UZlV79qBLM9JFnJuJ03gIi5whrob0xV0ofNVHy9DhwGdsMJQc2OKv0oGmLzvaqvAVv+g==", - "dev": true, - "peer": true, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/cli-table3": { "version": "0.6.2", "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.2.tgz", @@ -6135,48 +2796,11 @@ "node": ">=8" } }, - "node_modules/clone": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.1.tgz", - "integrity": "sha512-h5FLmEMFHeuzqmpVRcDayNlVZ+k4uK1niyKQN6oUMe7ieJihv44Vc3dY/kDnnWX4PDQSwes48s965PG/D4GntQ==", - "dev": true, - "engines": { - "node": ">=0.8" - } - }, - "node_modules/clone-deep": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", - "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", - "dev": true, - "peer": true, - "dependencies": { - "is-plain-object": "^2.0.4", - "kind-of": "^6.0.2", - "shallow-clone": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/collection-visit": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz", - "integrity": "sha512-lNkKvzEeMBBjUGHZ+q6z9pSJla0KWAQPvtzhEV9+iGyQYG+pBpl7xKDhxoNSOZH2hhv0v5k0y2yAM4o4SjoSkw==", - "dev": true, - "peer": true, - "dependencies": { - "map-visit": "^1.0.0", - "object-visit": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, "dependencies": { "color-name": "~1.1.4" }, @@ -6187,7 +2811,8 @@ "node_modules/color-name": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true }, "node_modules/colorette": { "version": "1.4.0", @@ -6207,13 +2832,6 @@ "node": ">= 0.8" } }, - "node_modules/command-exists": { - "version": "1.2.9", - "resolved": "https://registry.npmjs.org/command-exists/-/command-exists-1.2.9.tgz", - "integrity": "sha512-LTQ/SGc+s0Xc0Fu5WaKnR0YiygZkm9eKFvyS+fRsU7/ZWFF8ykFM6Pc9aCVf1+xasOOZpO3BAVgVrKvsqKHV7w==", - "dev": true, - "peer": true - }, "node_modules/commander": { "version": "6.2.1", "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz", @@ -6232,122 +2850,11 @@ "node": ">=4.0.0" } }, - "node_modules/commondir": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", - "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==", - "dev": true, - "peer": true - }, - "node_modules/component-emitter": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz", - "integrity": "sha512-jPatnhd33viNplKjqXKRkGU345p263OIWzDL2wH3LGIGp5Kojo+uXizHmOADRvhGFFTnJqX3jBAKP6vvmSDKcA==", - "dev": true - }, - "node_modules/compressible": { - "version": "2.0.18", - "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", - "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", - "dev": true, - "peer": true, - "dependencies": { - "mime-db": ">= 1.43.0 < 2" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/compression": { - "version": "1.7.4", - "resolved": "https://registry.npmjs.org/compression/-/compression-1.7.4.tgz", - "integrity": "sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ==", - "dev": true, - "peer": true, - "dependencies": { - "accepts": "~1.3.5", - "bytes": "3.0.0", - "compressible": "~2.0.16", - "debug": "2.6.9", - "on-headers": "~1.0.2", - "safe-buffer": "5.1.2", - "vary": "~1.1.2" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/compression/node_modules/bytes": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", - "integrity": "sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==", - "dev": true, - "peer": true, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/compression/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "peer": true, - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/compression/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true, - "peer": true - }, - "node_modules/compression/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true, - "peer": true - }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" - }, - "node_modules/connect": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/connect/-/connect-3.7.0.tgz", - "integrity": "sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ==", - "dev": true, - "peer": true, - "dependencies": { - "debug": "2.6.9", - "finalhandler": "1.1.2", - "parseurl": "~1.3.3", - "utils-merge": "1.0.1" - }, - "engines": { - "node": ">= 0.10.0" - } - }, - "node_modules/connect/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "peer": true, - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/connect/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true, - "peer": true + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true }, "node_modules/content-disposition": { "version": "0.5.4", @@ -6402,88 +2909,12 @@ "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=", "dev": true }, - "node_modules/copy-descriptor": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz", - "integrity": "sha512-XgZ0pFcakEUlbwQEVNg3+QAis1FyTL3Qel9FYy8pSkQqoG3PNoT0bOCQtOXcOkur21r2Eq2kI+IE+gsmAEVlYw==", - "dev": true, - "peer": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/core-js": { - "version": "3.8.3", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.8.3.tgz", - "integrity": "sha512-KPYXeVZYemC2TkNEkX/01I+7yd+nX3KddKwZ1Ww7SKWdI2wQprSgLmrTddT8nw92AjEklTsPBoSdQBhbI1bQ6Q==", - "deprecated": "core-js@<3.23.3 is no longer maintained and not recommended for usage due to the number of issues. Because of the V8 engine whims, feature detection in old core-js versions could cause a slowdown up to 100x even if nothing is polyfilled. Some versions have web compatibility issues. Please, upgrade your dependencies to the actual version of core-js.", - "dev": true, - "hasInstallScript": true, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/core-js" - } - }, - "node_modules/core-js-compat": { - "version": "3.29.1", - "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.29.1.tgz", - "integrity": "sha512-QmchCua884D8wWskMX8tW5ydINzd8oSJVx38lx/pVkFGqztxt73GYre3pm/hyYq8bPf+MW5In4I/uRShFDsbrA==", - "dev": true, - "peer": true, - "dependencies": { - "browserslist": "^4.21.5" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/core-js" - } - }, "node_modules/core-util-is": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", "dev": true }, - "node_modules/cosmiconfig": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-5.2.1.tgz", - "integrity": "sha512-H65gsXo1SKjf8zmrJ67eJk8aIRKV5ff2D4uKZIBZShbhGSpEmsQOPW/SKMKYhSTrqR7ufy6RP69rPogdaPh/kA==", - "dev": true, - "peer": true, - "dependencies": { - "import-fresh": "^2.0.0", - "is-directory": "^0.3.1", - "js-yaml": "^3.13.1", - "parse-json": "^4.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/cosmiconfig/node_modules/argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "dev": true, - "peer": true, - "dependencies": { - "sprintf-js": "~1.0.2" - } - }, - "node_modules/cosmiconfig/node_modules/js-yaml": { - "version": "3.14.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", - "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", - "dev": true, - "peer": true, - "dependencies": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, "node_modules/cross-env": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz", @@ -6506,6 +2937,7 @@ "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -6531,15 +2963,15 @@ "dev": true }, "node_modules/cypress": { - "version": "12.11.0", - "resolved": "https://registry.npmjs.org/cypress/-/cypress-12.11.0.tgz", - "integrity": "sha512-TJE+CCWI26Hwr5Msb9GpQhFLubdYooW0fmlPwTsfiyxmngqc7+SZGLPeIkj2dTSSZSEtpQVzOzvcnzH0o8G7Vw==", + "version": "13.3.0", + "resolved": "https://registry.npmjs.org/cypress/-/cypress-13.3.0.tgz", + "integrity": "sha512-mpI8qcTwLGiA4zEQvTC/U1xGUezVV4V8HQCOYjlEOrVmU1etVvxOjkCXHGwrlYdZU/EPmUiWfsO3yt1o+Q2bgw==", "dev": true, "hasInstallScript": true, "dependencies": { - "@cypress/request": "^2.88.10", + "@cypress/request": "^3.0.0", "@cypress/xvfb": "^1.2.4", - "@types/node": "^14.14.31", + "@types/node": "^18.17.5", "@types/sinonjs__fake-timers": "8.1.1", "@types/sizzle": "^2.3.2", "arch": "^2.2.0", @@ -6572,9 +3004,10 @@ "minimist": "^1.2.8", "ospath": "^1.2.2", "pretty-bytes": "^5.6.0", + "process": "^0.11.10", "proxy-from-env": "1.0.0", "request-progress": "^3.0.0", - "semver": "^7.3.2", + "semver": "^7.5.3", "supports-color": "^8.1.1", "tmp": "~0.2.1", "untildify": "^4.0.0", @@ -6584,7 +3017,7 @@ "cypress": "bin/cypress" }, "engines": { - "node": "^14.0.0 || ^16.0.0 || >=18.0.0" + "node": "^16.0.0 || ^18.0.0 || >=20.0.0" } }, "node_modules/cypress-file-upload": { @@ -6625,9 +3058,9 @@ } }, "node_modules/cypress-wait-until": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/cypress-wait-until/-/cypress-wait-until-1.7.2.tgz", - "integrity": "sha512-uZ+M8/MqRcpf+FII/UZrU7g1qYZ4aVlHcgyVopnladyoBrpoaMJ4PKZDrdOJ05H5RHbr7s9Tid635X3E+ZLU/Q==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/cypress-wait-until/-/cypress-wait-until-2.0.1.tgz", + "integrity": "sha512-+IyVnYNiaX1+C+V/LazrJWAi/CqiwfNoRSrFviECQEyolW1gDRy765PZosL2alSSGK8V10Y7BGfOQyZUDgmnjQ==", "dev": true }, "node_modules/cypress/node_modules/buffer": { @@ -6655,9 +3088,9 @@ } }, "node_modules/cypress/node_modules/semver": { - "version": "7.3.7", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz", - "integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==", + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", "dev": true, "dependencies": { "lru-cache": "^6.0.0" @@ -6712,9 +3145,9 @@ } }, "node_modules/dayjs": { - "version": "1.11.7", - "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.7.tgz", - "integrity": "sha512-+Yw9U6YO5TQohxLcIkrXBeY73WP3ejHWVvx8XCk3gxvQDCTEmS48ZrSZCKciI7Bhl/uCMyxYtE9UqRILmFphkQ==", + "version": "1.11.10", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.10.tgz", + "integrity": "sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ==", "dev": true }, "node_modules/debug": { @@ -6746,20 +3179,10 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/decode-uri-component": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", - "integrity": "sha512-hjf+xovcEn31w/EUYdTXQh/8smFL/dzYjohQGEIgjyNavaJfBY2p5F527Bo1VPATxv0VYTUC2bOcXvqFwk78Og==", - "dev": true, - "peer": true, - "engines": { - "node": ">=0.10" - } - }, "node_modules/deep-eql": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.2.tgz", - "integrity": "sha512-gT18+YW4CcW/DBNTwAmqTtkJh7f9qqScu2qFVlx7kCoeY9tlBu9cUcr7+I+Z/noG8INehS3xQgLpTtd/QUTn4w==", + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.3.tgz", + "integrity": "sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw==", "dev": true, "dependencies": { "type-detect": "^4.0.0" @@ -6817,32 +3240,27 @@ "node": ">=0.10.0" } }, - "node_modules/defaults": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.3.tgz", - "integrity": "sha512-s82itHOnYrN0Ib8r+z7laQz3sdE+4FP3d9Q7VLO7U+KRT+CR0GsWuyHxzdAY82I7cXv0G/twrqomTJLOssO5HA==", + "node_modules/define-data-property": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.0.tgz", + "integrity": "sha512-UzGwzcjyv3OtAvolTj1GoyNYzfFR+iqbGjcnBEENZVCpM4/Ng1yhGNvS3lR/xDS74Tb2wGG9WzNSNIOS9UVb2g==", "dev": true, - "peer": true, "dependencies": { - "clone": "^1.0.2" - } - }, - "node_modules/defaults/node_modules/clone": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", - "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", - "dev": true, - "peer": true, + "get-intrinsic": "^1.2.1", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.0" + }, "engines": { - "node": ">=0.8" + "node": ">= 0.4" } }, "node_modules/define-properties": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.0.tgz", - "integrity": "sha512-xvqAVKGfT1+UAvPwKTVw/njhdQ8ZhXK4lI0bCIuCMrp2up9nPnaDftrLtmpTazqd1o+UY4zgzU+avtMbDP+ldA==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", "dev": true, "dependencies": { + "define-data-property": "^1.0.1", "has-property-descriptors": "^1.0.0", "object-keys": "^1.1.1" }, @@ -6853,20 +3271,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/define-property": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", - "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==", - "dev": true, - "peer": true, - "dependencies": { - "is-descriptor": "^1.0.2", - "isobject": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -6876,13 +3280,6 @@ "node": ">=0.4.0" } }, - "node_modules/denodeify": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/denodeify/-/denodeify-1.2.1.tgz", - "integrity": "sha512-KNTihKNmQENUZeKu5fzfpzRqR5S2VMp4gl9RFHiWzj9DfvYQPMJ6XHKNaQxaGCXwPk6y9yme3aUoaiAe+KX+vg==", - "dev": true, - "peer": true - }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -6936,18 +3333,21 @@ } }, "node_modules/dom-accessibility-api": { - "version": "0.5.14", - "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.14.tgz", - "integrity": "sha512-NMt+m9zFMPZe0JcY9gN224Qvk6qLIdqex29clBvc/y75ZBX9YA9wNK3frsYvu2DI1xcCIwxwnX+TlsJ2DSOADg==", + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", + "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", "dev": true }, "node_modules/dotenv": { - "version": "16.0.3", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.0.3.tgz", - "integrity": "sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ==", + "version": "16.3.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.3.1.tgz", + "integrity": "sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ==", "dev": true, "engines": { "node": ">=12" + }, + "funding": { + "url": "https://github.com/motdotla/dotenv?sponsor=1" } }, "node_modules/ecc-jsbn": { @@ -7023,66 +3423,20 @@ "node": ">=8.6" } }, - "node_modules/envinfo": { - "version": "7.8.1", - "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.8.1.tgz", - "integrity": "sha512-/o+BXHmB7ocbHEAs6F2EnG0ogybVVUdkRunTT2glZU9XAaGmhqskrvKwqXuDfNjEO0LZKWdejEEpnq8aM0tOaw==", - "dev": true, - "peer": true, - "bin": { - "envinfo": "dist/cli.js" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/error-ex": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", - "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", - "dev": true, - "peer": true, - "dependencies": { - "is-arrayish": "^0.2.1" - } - }, - "node_modules/error-stack-parser": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/error-stack-parser/-/error-stack-parser-2.1.4.tgz", - "integrity": "sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ==", - "dev": true, - "peer": true, - "dependencies": { - "stackframe": "^1.3.4" - } - }, - "node_modules/errorhandler": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/errorhandler/-/errorhandler-1.5.1.tgz", - "integrity": "sha512-rcOwbfvP1WTViVoUjcfZicVzjhjTuhSMntHh6mW3IrEiyE6mJyXvsToJUJGlGlw/2xU9P5whlWNGlIDVeCiT4A==", - "dev": true, - "peer": true, - "dependencies": { - "accepts": "~1.3.7", - "escape-html": "~1.0.3" - }, - "engines": { - "node": ">= 0.8" - } - }, "node_modules/es-abstract": { - "version": "1.21.2", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.21.2.tgz", - "integrity": "sha512-y/B5POM2iBnIxCiernH1G7rC9qQoM77lLIMQLuob0zhp8C56Po81+2Nj0WFKnd0pNReDTnkYryc+zhOzpEIROg==", + "version": "1.22.2", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.22.2.tgz", + "integrity": "sha512-YoxfFcDmhjOgWPWsV13+2RNjq1F6UQnfs+8TftwNqtzlmFzEXvlUwdrNrYeaizfjQzRMxkZ6ElWMOJIFKdVqwA==", "dev": true, "dependencies": { "array-buffer-byte-length": "^1.0.0", + "arraybuffer.prototype.slice": "^1.0.2", "available-typed-arrays": "^1.0.5", "call-bind": "^1.0.2", "es-set-tostringtag": "^2.0.1", "es-to-primitive": "^1.2.1", - "function.prototype.name": "^1.1.5", - "get-intrinsic": "^1.2.0", + "function.prototype.name": "^1.1.6", + "get-intrinsic": "^1.2.1", "get-symbol-description": "^1.0.0", "globalthis": "^1.0.3", "gopd": "^1.0.1", @@ -7097,19 +3451,23 @@ "is-regex": "^1.1.4", "is-shared-array-buffer": "^1.0.2", "is-string": "^1.0.7", - "is-typed-array": "^1.1.10", + "is-typed-array": "^1.1.12", "is-weakref": "^1.0.2", "object-inspect": "^1.12.3", "object-keys": "^1.1.1", "object.assign": "^4.1.4", - "regexp.prototype.flags": "^1.4.3", + "regexp.prototype.flags": "^1.5.1", + "safe-array-concat": "^1.0.1", "safe-regex-test": "^1.0.0", - "string.prototype.trim": "^1.2.7", - "string.prototype.trimend": "^1.0.6", - "string.prototype.trimstart": "^1.0.6", + "string.prototype.trim": "^1.2.8", + "string.prototype.trimend": "^1.0.7", + "string.prototype.trimstart": "^1.0.7", + "typed-array-buffer": "^1.0.0", + "typed-array-byte-length": "^1.0.0", + "typed-array-byte-offset": "^1.0.0", "typed-array-length": "^1.0.4", "unbox-primitive": "^1.0.2", - "which-typed-array": "^1.1.9" + "which-typed-array": "^1.1.11" }, "engines": { "node": ">= 0.4" @@ -7144,6 +3502,28 @@ "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", "dev": true }, + "node_modules/es-iterator-helpers": { + "version": "1.0.15", + "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.0.15.tgz", + "integrity": "sha512-GhoY8uYqd6iwUl2kgjTm4CZAf6oo5mHK7BPqx3rKgx893YSsy0LGHV6gfqqQvZt/8xM8xeOnfXBCfqclMKkJ5g==", + "dev": true, + "dependencies": { + "asynciterator.prototype": "^1.0.0", + "call-bind": "^1.0.2", + "define-properties": "^1.2.1", + "es-abstract": "^1.22.1", + "es-set-tostringtag": "^2.0.1", + "function-bind": "^1.1.1", + "get-intrinsic": "^1.2.1", + "globalthis": "^1.0.3", + "has-property-descriptors": "^1.0.0", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "internal-slot": "^1.0.5", + "iterator.prototype": "^1.1.2", + "safe-array-concat": "^1.0.1" + } + }, "node_modules/es-module-lexer": { "version": "0.9.3", "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-0.9.3.tgz", @@ -7216,27 +3596,27 @@ } }, "node_modules/eslint": { - "version": "8.39.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.39.0.tgz", - "integrity": "sha512-mwiok6cy7KTW7rBpo05k6+p4YVZByLNjAZ/ACB9DRCu4YDRwjXI01tWHp6KAUWelsBetTxKK/2sHB0vdS8Z2Og==", + "version": "8.50.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.50.0.tgz", + "integrity": "sha512-FOnOGSuFuFLv/Sa+FDVRZl4GGVAAFFi8LecRsI5a1tMO5HIE8nCm4ivAlzt4dT3ol/PaaGC0rJEEXQmHJBGoOg==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.4.0", - "@eslint/eslintrc": "^2.0.2", - "@eslint/js": "8.39.0", - "@humanwhocodes/config-array": "^0.11.8", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.2", + "@eslint/js": "8.50.0", + "@humanwhocodes/config-array": "^0.11.11", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", - "ajv": "^6.10.0", + "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.2", "debug": "^4.3.2", "doctrine": "^3.0.0", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^7.2.0", - "eslint-visitor-keys": "^3.4.0", - "espree": "^9.5.1", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", "esquery": "^1.4.2", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", @@ -7244,22 +3624,19 @@ "find-up": "^5.0.0", "glob-parent": "^6.0.2", "globals": "^13.19.0", - "grapheme-splitter": "^1.0.4", + "graphemer": "^1.4.0", "ignore": "^5.2.0", - "import-fresh": "^3.0.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "is-path-inside": "^3.0.3", - "js-sdsl": "^4.1.4", "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", + "optionator": "^0.9.3", "strip-ansi": "^6.0.1", - "strip-json-comments": "^3.1.0", "text-table": "^0.2.0" }, "bin": { @@ -7310,22 +3687,22 @@ } }, "node_modules/eslint-import-resolver-webpack": { - "version": "0.13.2", - "resolved": "https://registry.npmjs.org/eslint-import-resolver-webpack/-/eslint-import-resolver-webpack-0.13.2.tgz", - "integrity": "sha512-XodIPyg1OgE2h5BDErz3WJoK7lawxKTJNhgPNafRST6csC/MZC+L5P6kKqsZGRInpbgc02s/WZMrb4uGJzcuRg==", + "version": "0.13.7", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-webpack/-/eslint-import-resolver-webpack-0.13.7.tgz", + "integrity": "sha512-2a+meyMeABBRO4K53Oj1ygkmt5lhQS79Lmx2f684Qnv6gjvD4RLOM5jfPGTXwQ0A2K03WSoKt3HRQu/uBgxF7w==", "dev": true, "dependencies": { - "array-find": "^1.0.0", + "array.prototype.find": "^2.2.1", "debug": "^3.2.7", "enhanced-resolve": "^0.9.1", "find-root": "^1.1.0", "has": "^1.0.3", "interpret": "^1.4.0", - "is-core-module": "^2.7.0", + "is-core-module": "^2.13.0", "is-regex": "^1.1.4", "lodash": "^4.17.21", - "resolve": "^1.20.0", - "semver": "^5.7.1" + "resolve": "^2.0.0-next.4", + "semver": "^5.7.2" }, "engines": { "node": ">= 6" @@ -7353,19 +3730,36 @@ "node": ">= 0.10" } }, + "node_modules/eslint-import-resolver-webpack/node_modules/resolve": { + "version": "2.0.0-next.4", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.4.tgz", + "integrity": "sha512-iMDbmAWtfU+MHpxt/I5iWI7cY6YVEZUQ3MBgPQ++XD1PELuJHIl82xBmObyP2KyQmkNB2dsqF7seoQQiAn5yDQ==", + "dev": true, + "dependencies": { + "is-core-module": "^2.9.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/eslint-import-resolver-webpack/node_modules/semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", "dev": true, "bin": { "semver": "bin/semver" } }, "node_modules/eslint-module-utils": { - "version": "2.7.4", - "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.7.4.tgz", - "integrity": "sha512-j4GT+rqzCoRKHwURX7pddtIPGySnX9Si/cgMI5ztrcqOPtk5dDEeZ34CQVPphnqkJytlc97Vuk05Um2mJ3gEQA==", + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.8.0.tgz", + "integrity": "sha512-aWajIYfsqCKRDgUfjEXNN/JlrzauMuSEy5sbd7WXbtW3EH6A6MpwEh42c7qD+MqQo9QMJ6fWLAeIJynx0g6OAw==", "dev": true, "dependencies": { "debug": "^3.2.7" @@ -7389,17 +3783,44 @@ } }, "node_modules/eslint-plugin-cypress": { - "version": "2.13.3", - "resolved": "https://registry.npmjs.org/eslint-plugin-cypress/-/eslint-plugin-cypress-2.13.3.tgz", - "integrity": "sha512-nAPjZE5WopCsgJwl3vHm5iafpV+ZRO76Z9hMyRygWhmg5ODXDPd+9MaPl7kdJ2azj+sO87H3P1PRnggIrz848g==", + "version": "2.15.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-cypress/-/eslint-plugin-cypress-2.15.1.tgz", + "integrity": "sha512-eLHLWP5Q+I4j2AWepYq0PgFEei9/s5LvjuSqWrxurkg1YZ8ltxdvMNmdSf0drnsNo57CTgYY/NIHHLRSWejR7w==", "dev": true, "dependencies": { - "globals": "^11.12.0" + "globals": "^13.20.0" }, "peerDependencies": { "eslint": ">= 3.2.1" } }, + "node_modules/eslint-plugin-cypress/node_modules/globals": { + "version": "13.21.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.21.0.tgz", + "integrity": "sha512-ybyme3s4yy/t/3s35bewwXKOf7cvzfreG2lH0lZl0JB7I4GxRP2ghxOK/Nb9EkRXdbBXZLfq/p/0W2JUONB/Gg==", + "dev": true, + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint-plugin-cypress/node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/eslint-plugin-header": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/eslint-plugin-header/-/eslint-plugin-header-3.1.1.tgz", @@ -7410,26 +3831,28 @@ } }, "node_modules/eslint-plugin-import": { - "version": "2.27.5", - "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.27.5.tgz", - "integrity": "sha512-LmEt3GVofgiGuiE+ORpnvP+kAm3h6MLZJ4Q5HCyHADofsb4VzXFsRiWj3c0OFiV+3DWFh0qg3v9gcPlfc3zRow==", + "version": "2.28.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.28.1.tgz", + "integrity": "sha512-9I9hFlITvOV55alzoKBI+K9q74kv0iKMeY6av5+umsNwayt59fz692daGyjR+oStBQgx6nwR9rXldDev3Clw+A==", "dev": true, "dependencies": { "array-includes": "^3.1.6", + "array.prototype.findlastindex": "^1.2.2", "array.prototype.flat": "^1.3.1", "array.prototype.flatmap": "^1.3.1", "debug": "^3.2.7", "doctrine": "^2.1.0", "eslint-import-resolver-node": "^0.3.7", - "eslint-module-utils": "^2.7.4", + "eslint-module-utils": "^2.8.0", "has": "^1.0.3", - "is-core-module": "^2.11.0", + "is-core-module": "^2.13.0", "is-glob": "^4.0.3", "minimatch": "^3.1.2", + "object.fromentries": "^2.0.6", + "object.groupby": "^1.0.0", "object.values": "^1.1.6", - "resolve": "^1.22.1", - "semver": "^6.3.0", - "tsconfig-paths": "^3.14.1" + "semver": "^6.3.1", + "tsconfig-paths": "^3.14.2" }, "engines": { "node": ">=4" @@ -7459,21 +3882,13 @@ "node": ">=0.10.0" } }, - "node_modules/eslint-plugin-import/node_modules/resolve": { - "version": "1.22.1", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz", - "integrity": "sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==", + "node_modules/eslint-plugin-import/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, - "dependencies": { - "is-core-module": "^2.9.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, "bin": { - "resolve": "bin/resolve" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "semver": "bin/semver.js" } }, "node_modules/eslint-plugin-jsx-a11y": { @@ -7533,15 +3948,16 @@ } }, "node_modules/eslint-plugin-react": { - "version": "7.32.2", - "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.32.2.tgz", - "integrity": "sha512-t2fBMa+XzonrrNkyVirzKlvn5RXzzPwRHtMvLAtVZrt8oxgnTQaYbU6SXTOO1mwQgp1y5+toMSKInnzGr0Knqg==", + "version": "7.33.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.33.2.tgz", + "integrity": "sha512-73QQMKALArI8/7xGLNI/3LylrEYrlKZSb5C9+q3OtOewTnMQi5cT+aE9E41sLCmli3I9PGGmD1yiZydyo4FEPw==", "dev": true, "dependencies": { "array-includes": "^3.1.6", "array.prototype.flatmap": "^1.3.1", "array.prototype.tosorted": "^1.1.1", "doctrine": "^2.1.0", + "es-iterator-helpers": "^1.0.12", "estraverse": "^5.3.0", "jsx-ast-utils": "^2.4.1 || ^3.0.0", "minimatch": "^3.1.2", @@ -7551,7 +3967,7 @@ "object.values": "^1.1.6", "prop-types": "^15.8.1", "resolve": "^2.0.0-next.4", - "semver": "^6.3.0", + "semver": "^6.3.1", "string.prototype.matchall": "^4.0.8" }, "engines": { @@ -7590,6 +4006,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/eslint-plugin-react/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, "node_modules/eslint-rule-composer": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/eslint-rule-composer/-/eslint-rule-composer-0.3.0.tgz", @@ -7622,9 +4047,9 @@ } }, "node_modules/eslint-visitor-keys": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.0.tgz", - "integrity": "sha512-HPpKPUBQcAsZOsHAFwTtIKcYlCje62XB7SEAcxjtmW6TD1WVpkS6i6/hOVtTZIl4zGj/mBqpFVGvaDneik+VoQ==", + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", "dev": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" @@ -7646,9 +4071,9 @@ } }, "node_modules/eslint/node_modules/eslint-scope": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.0.tgz", - "integrity": "sha512-DYj5deGlHBfMt15J7rdtyKNq/Nqlv5KfU4iodrQ019XESsRnwXH9KAE0y3cwtUHDo2ob7CypAnCqefh6vioWRw==", + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", "dev": true, "dependencies": { "esrecurse": "^4.3.0", @@ -7688,31 +4113,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/eslint/node_modules/import-fresh": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", - "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", - "dev": true, - "dependencies": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint/node_modules/resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true, - "engines": { - "node": ">=4" - } - }, "node_modules/eslint/node_modules/strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", @@ -7747,14 +4147,14 @@ } }, "node_modules/espree": { - "version": "9.5.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.5.1.tgz", - "integrity": "sha512-5yxtHSZXRSW5pvv3hAlXM5+/Oswi1AUFqBmbibKb5s6bp3rGIDkyXU6xCoyuuLhijr4SFwPrXRoZjz0AZDN9tg==", + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", "dev": true, "dependencies": { - "acorn": "^8.8.0", + "acorn": "^8.9.0", "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^3.4.0" + "eslint-visitor-keys": "^3.4.1" }, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" @@ -7763,20 +4163,6 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/esprima": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "dev": true, - "peer": true, - "bin": { - "esparse": "bin/esparse.js", - "esvalidate": "bin/esvalidate.js" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/esquery": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", @@ -7828,16 +4214,6 @@ "node": ">= 0.6" } }, - "node_modules/event-target-shim": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", - "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", - "dev": true, - "peer": true, - "engines": { - "node": ">=6" - } - }, "node_modules/eventemitter2": { "version": "6.4.7", "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-6.4.7.tgz", @@ -7897,155 +4273,6 @@ "node": ">= 0.8.0" } }, - "node_modules/expand-brackets": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", - "integrity": "sha512-w/ozOKR9Obk3qoWeY/WDi6MFta9AoMR+zud60mdnbniMcBxRuFJyDt2LdX/14A1UABeqk+Uk+LDfUpvoGKppZA==", - "dev": true, - "peer": true, - "dependencies": { - "debug": "^2.3.3", - "define-property": "^0.2.5", - "extend-shallow": "^2.0.1", - "posix-character-classes": "^0.1.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/expand-brackets/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "peer": true, - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/expand-brackets/node_modules/define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA==", - "dev": true, - "peer": true, - "dependencies": { - "is-descriptor": "^0.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/expand-brackets/node_modules/extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", - "dev": true, - "peer": true, - "dependencies": { - "is-extendable": "^0.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/expand-brackets/node_modules/is-accessor-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", - "integrity": "sha512-e1BM1qnDbMRG3ll2U9dSK0UMHuWOs3pY3AtcFsmvwPtKL3MML/Q86i+GilLfvqEs4GW+ExB91tQ3Ig9noDIZ+A==", - "dev": true, - "peer": true, - "dependencies": { - "kind-of": "^3.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/expand-brackets/node_modules/is-accessor-descriptor/node_modules/kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", - "dev": true, - "peer": true, - "dependencies": { - "is-buffer": "^1.1.5" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/expand-brackets/node_modules/is-data-descriptor": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", - "integrity": "sha512-+w9D5ulSoBNlmw9OHn3U2v51SyoCd0he+bB3xMl62oijhrspxowjU+AIcDY0N3iEJbUEkB15IlMASQsxYigvXg==", - "dev": true, - "peer": true, - "dependencies": { - "kind-of": "^3.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/expand-brackets/node_modules/is-data-descriptor/node_modules/kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", - "dev": true, - "peer": true, - "dependencies": { - "is-buffer": "^1.1.5" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/expand-brackets/node_modules/is-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", - "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", - "dev": true, - "peer": true, - "dependencies": { - "is-accessor-descriptor": "^0.1.6", - "is-data-descriptor": "^0.1.4", - "kind-of": "^5.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/expand-brackets/node_modules/is-extendable": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", - "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", - "dev": true, - "peer": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/expand-brackets/node_modules/kind-of": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", - "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", - "dev": true, - "peer": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/expand-brackets/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true, - "peer": true - }, "node_modules/express": { "version": "4.18.2", "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz", @@ -8163,76 +4390,6 @@ "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", "dev": true }, - "node_modules/extend-shallow": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", - "integrity": "sha512-BwY5b5Ql4+qZoefgMj2NUmx+tehVTH/Kf4k1ZEtOHNFcm2wSxMRo992l6X3TIgni2eZVTZ85xMOjF31fwZAj6Q==", - "dev": true, - "peer": true, - "dependencies": { - "assign-symbols": "^1.0.0", - "is-extendable": "^1.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/extglob": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", - "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", - "dev": true, - "peer": true, - "dependencies": { - "array-unique": "^0.3.2", - "define-property": "^1.0.0", - "expand-brackets": "^2.1.4", - "extend-shallow": "^2.0.1", - "fragment-cache": "^0.2.1", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/extglob/node_modules/define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha512-cZTYKFWspt9jZsMscWo8sc/5lbPC9Q0N5nBLgb+Yd915iL3udB1uFgS3B8YCx66UVHq018DAVFoee7x+gxggeA==", - "dev": true, - "peer": true, - "dependencies": { - "is-descriptor": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/extglob/node_modules/extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", - "dev": true, - "peer": true, - "dependencies": { - "is-extendable": "^0.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/extglob/node_modules/is-extendable": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", - "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", - "dev": true, - "peer": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/extract-zip": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", @@ -8269,9 +4426,9 @@ "dev": true }, "node_modules/fast-glob": { - "version": "3.2.12", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.12.tgz", - "integrity": "sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz", + "integrity": "sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==", "dev": true, "dependencies": { "@nodelib/fs.stat": "^2.0.2", @@ -8305,16 +4462,6 @@ "reusify": "^1.0.4" } }, - "node_modules/fb-watchman": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.1.tgz", - "integrity": "sha512-DkPJKQeY6kKwmuMretBhr7G6Vodr7bFwDYTXIkfG1gjvNpaxBTQV3PbXg6bR1c1UP4jPOX0jHUbbHANL9vRjVg==", - "dev": true, - "peer": true, - "dependencies": { - "bser": "2.1.1" - } - }, "node_modules/fd-slicer": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", @@ -8355,6 +4502,7 @@ "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, "dependencies": { "to-regex-range": "^5.0.1" }, @@ -8362,57 +4510,6 @@ "node": ">=8" } }, - "node_modules/finalhandler": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", - "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", - "dev": true, - "peer": true, - "dependencies": { - "debug": "2.6.9", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "on-finished": "~2.3.0", - "parseurl": "~1.3.3", - "statuses": "~1.5.0", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/finalhandler/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "peer": true, - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/finalhandler/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true, - "peer": true - }, - "node_modules/find-cache-dir": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-2.1.0.tgz", - "integrity": "sha512-Tq6PixE0w/VMFfCgbONnkiQIVol/JJL7nRMi20fqzA4NRs9AfeqMGeRdPi3wIhYkxjeBaWh2rxwapn5Tu3IqOQ==", - "dev": true, - "peer": true, - "dependencies": { - "commondir": "^1.0.1", - "make-dir": "^2.0.0", - "pkg-dir": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/find-root": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", @@ -8435,14 +4532,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/find-yarn-workspace-root": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/find-yarn-workspace-root/-/find-yarn-workspace-root-2.0.0.tgz", - "integrity": "sha512-1IMnbjt4KzsQfnhnzNd8wUEgXZ44IzZaZmnLYx7D5FZlaHt2gW20Cri8Q+E/t5tIj4+epTBub+2Zxu/vNILzqQ==", - "dependencies": { - "micromatch": "^4.0.2" - } - }, "node_modules/flat": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", @@ -8486,16 +4575,6 @@ "integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==", "dev": true }, - "node_modules/flow-parser": { - "version": "0.121.0", - "resolved": "https://registry.npmjs.org/flow-parser/-/flow-parser-0.121.0.tgz", - "integrity": "sha512-1gIBiWJNR0tKUNv8gZuk7l9rVX06OuLzY9AoGio7y/JT4V1IZErEMEq2TJS+PFcw/y0RshZ1J/27VfK1UQzYVg==", - "dev": true, - "peer": true, - "engines": { - "node": ">=0.4.0" - } - }, "node_modules/follow-redirects": { "version": "1.15.1", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.1.tgz", @@ -8525,16 +4604,6 @@ "is-callable": "^1.1.3" } }, - "node_modules/for-in": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", - "integrity": "sha512-7EwmXrOjyL+ChxMhmG5lnW9MPt1aIeZEwKhQzoBUdTV0N3zuwWDZYVJatDvZ2OyzPUvdIAZDsCetk3coyMfcnQ==", - "dev": true, - "peer": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/forever-agent": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", @@ -8567,19 +4636,6 @@ "node": ">= 0.6" } }, - "node_modules/fragment-cache": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz", - "integrity": "sha512-GMBAbW9antB8iZRHLoGw0b3HANt57diZYFO/HL1JGIC1MjKrdmhxvrJbupnVvpys0zsz7yBApXdQyfepKly2kA==", - "dev": true, - "peer": true, - "dependencies": { - "map-cache": "^0.2.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/fresh": { "version": "0.5.2", "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", @@ -8593,6 +4649,7 @@ "version": "9.1.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, "dependencies": { "at-least-node": "^1.0.0", "graceful-fs": "^4.2.0", @@ -8606,7 +4663,8 @@ "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "dev": true }, "node_modules/fsevents": { "version": "2.3.2", @@ -8635,15 +4693,15 @@ "dev": true }, "node_modules/function.prototype.name": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.5.tgz", - "integrity": "sha512-uN7m/BzVKQnCUF/iW8jYea67v++2u7m5UgENbHRtdDVclOUP+FMPlCNdmk0h/ysGyo2tavMJEDqJAkJdRa1vMA==", + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.6.tgz", + "integrity": "sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg==", "dev": true, "dependencies": { "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "es-abstract": "^1.19.0", - "functions-have-names": "^1.2.2" + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "functions-have-names": "^1.2.3" }, "engines": { "node": ">= 0.4" @@ -8681,22 +4739,23 @@ } }, "node_modules/get-func-name": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", - "integrity": "sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", + "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==", "dev": true, "engines": { "node": "*" } }, "node_modules/get-intrinsic": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.0.tgz", - "integrity": "sha512-L049y6nFOuom5wGyRc3/gdTLO94dySVKRACj1RmJZBQXlbTMhtNIgkWkUHq+jYmZvKf14EW1EoJnnjbmoHij0Q==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz", + "integrity": "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==", "dev": true, "dependencies": { "function-bind": "^1.1.1", "has": "^1.0.3", + "has-proto": "^1.0.1", "has-symbols": "^1.0.3" }, "funding": { @@ -8712,12 +4771,6 @@ "node": ">=8.0.0" } }, - "node_modules/get-params": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/get-params/-/get-params-0.1.2.tgz", - "integrity": "sha512-41eOxtlGgHQRbFyA8KTH+w+32Em3cRdfBud7j67ulzmIfmaHX9doq47s0fa4P5o9H64BZX9nrYI6sJvk46Op+Q==", - "dev": true - }, "node_modules/get-stream": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", @@ -8749,16 +4802,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/get-value": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", - "integrity": "sha512-Ln0UQDlxH1BapMu3GPtf7CuYNwRZf2gwCuPqbyG6pB8WfmFpzqcy4xtAaAMUhnNqjMKTiCPZG2oMT3YSx8U2NA==", - "dev": true, - "peer": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/getopts": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/getopts/-/getopts-2.3.0.tgz", @@ -8783,19 +4826,11 @@ "assert-plus": "^1.0.0" } }, - "node_modules/gfycat-sdk": { - "version": "1.4.18", - "resolved": "https://registry.npmjs.org/gfycat-sdk/-/gfycat-sdk-1.4.18.tgz", - "integrity": "sha512-BrINtO6rj8Nr0pm38Qr3epayOuvlKcEFcDCw6yL9T4SrsWTECct6FS/isli766kdij2GGG6bWU6bRp+fDS2Cbg==", - "dev": true, - "dependencies": { - "babel-runtime": "^6.23.0" - } - }, "node_modules/glob": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", + "dev": true, "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -8850,6 +4885,7 @@ "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", "dev": true, + "peer": true, "engines": { "node": ">=4" } @@ -8904,12 +4940,13 @@ "node_modules/graceful-fs": { "version": "4.2.10", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", - "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==" + "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==", + "dev": true }, - "node_modules/grapheme-splitter": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz", - "integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==", + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", "dev": true }, "node_modules/has": { @@ -8937,6 +4974,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, "engines": { "node": ">=8" } @@ -8992,74 +5030,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/has-value": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz", - "integrity": "sha512-IBXk4GTsLYdQ7Rvt+GRBrFSVEkmuOUy4re0Xjd9kJSUQpnTrWR4/y9RpfexN9vkAPMFuQoeWKwqzPozRTlasGw==", - "dev": true, - "peer": true, - "dependencies": { - "get-value": "^2.0.6", - "has-values": "^1.0.0", - "isobject": "^3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/has-values": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-values/-/has-values-1.0.0.tgz", - "integrity": "sha512-ODYZC64uqzmtfGMEAX/FvZiRyWLpAC3vYnNunURUnkGVTS+mI0smVsWaPydRBsE3g+ok7h960jChO8mFcWlHaQ==", - "dev": true, - "peer": true, - "dependencies": { - "is-number": "^3.0.0", - "kind-of": "^4.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/has-values/node_modules/is-number": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", - "integrity": "sha512-4cboCqIpliH+mAvFNegjZQ4kgKc3ZUhQVr3HvWbSh5q3WH2v82ct+T2Y1hdU5Gdtorx/cLifQjqCbL7bpznLTg==", - "dev": true, - "peer": true, - "dependencies": { - "kind-of": "^3.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/has-values/node_modules/is-number/node_modules/kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", - "dev": true, - "peer": true, - "dependencies": { - "is-buffer": "^1.1.5" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/has-values/node_modules/kind-of": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz", - "integrity": "sha512-24XsCxmEbRwEDbz/qz3stgin8TTzZ1ESR56OMCN0ujYg+vRutNSiOj9bHH9u85DKgXguraugV5sFuvbD4FW/hw==", - "dev": true, - "peer": true, - "dependencies": { - "is-buffer": "^1.1.5" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/he": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", @@ -9069,43 +5039,6 @@ "he": "bin/he" } }, - "node_modules/hermes-engine": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/hermes-engine/-/hermes-engine-0.11.0.tgz", - "integrity": "sha512-7aMUlZja2IyLYAcZ69NBnwJAR5ZOYlSllj0oMpx08a8HzxHOys0eKCzfphrf6D0vX1JGO1QQvVsQKe6TkYherw==", - "dev": true, - "peer": true - }, - "node_modules/hermes-estree": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.6.0.tgz", - "integrity": "sha512-2YTGzJCkhdmT6VuNprWjXnvTvw/3iPNw804oc7yknvQpNKo+vJGZmtvLLCghOZf0OwzKaNAzeIMp71zQbNl09w==", - "dev": true, - "peer": true - }, - "node_modules/hermes-parser": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.6.0.tgz", - "integrity": "sha512-Vf58jBZca2+QBLR9h7B7mdg8oFz2g5ILz1iVouZ5DOrOrAfBmPfJjdjDT8jrO0f+iJ4/hSRrQHqHIjSnTaLUDQ==", - "dev": true, - "peer": true, - "dependencies": { - "hermes-estree": "0.6.0" - } - }, - "node_modules/hermes-profile-transformer": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/hermes-profile-transformer/-/hermes-profile-transformer-0.0.6.tgz", - "integrity": "sha512-cnN7bQUm65UWOy6cbGcCcZ3rpwW8Q/j4OP5aWRhEry4Z2t2aR1cjrbp0BS+KiBN0smvP1caBgAuxutvyvJILzQ==", - "dev": true, - "peer": true, - "dependencies": { - "source-map": "^0.7.3" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/http-errors": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", @@ -9181,39 +5114,12 @@ "node": ">= 4" } }, - "node_modules/image-size": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/image-size/-/image-size-0.6.3.tgz", - "integrity": "sha512-47xSUiQioGaB96nqtp5/q55m0aBQSQdyIloMOc/x+QVTDZLNmXE892IIDrJ0hM1A5vcNUDD5tDffkSP5lCaIIA==", - "dev": true, - "peer": true, - "bin": { - "image-size": "bin/image-size.js" - }, - "engines": { - "node": ">=4.0" - } - }, "node_modules/immediate": { "version": "3.0.6", "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", "integrity": "sha1-nbHb0Pr43m++D13V5Wu2BigN5ps=", "dev": true }, - "node_modules/import-fresh": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-2.0.0.tgz", - "integrity": "sha512-eZ5H8rcgYazHbKC3PG4ClHNykCSxtAhxSSEM+2mb+7evD2CKF5V7c0dNum7AdpDh0ZdICwZY9sRSn8f+KH96sg==", - "dev": true, - "peer": true, - "dependencies": { - "caller-path": "^2.0.0", - "resolve-from": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/imurmurhash": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", @@ -9236,6 +5142,7 @@ "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dev": true, "dependencies": { "once": "^1.3.0", "wrappy": "1" @@ -9244,7 +5151,8 @@ "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true }, "node_modules/ini": { "version": "2.0.0", @@ -9278,23 +5186,6 @@ "node": ">= 0.10" } }, - "node_modules/invariant": { - "version": "2.2.4", - "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", - "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", - "dev": true, - "peer": true, - "dependencies": { - "loose-envify": "^1.0.0" - } - }, - "node_modules/ip": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.8.tgz", - "integrity": "sha512-PuExPYUiu6qMBQb4l06ecm6T6ujzhmh+MeJcW9wa89PoAz5pvd4zPgN5WJV104mb6S2T1AwNIAaB70JNrLQWhg==", - "dev": true, - "peer": true - }, "node_modules/ip-regex": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-2.1.0.tgz", @@ -9313,19 +5204,6 @@ "node": ">= 0.10" } }, - "node_modules/is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "dev": true, - "peer": true, - "dependencies": { - "kind-of": "^6.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/is-arguments": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz", @@ -9356,12 +5234,20 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "node_modules/is-async-function": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.0.0.tgz", + "integrity": "sha512-Y1JXKrfykRJGdlDwdKlLpLyMIiWqWvuSd17TvZk68PLAOGOoF4Xyav1z0Xhoi+gCYjZVeC5SI+hYFOfvXmGRCA==", "dev": true, - "peer": true + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, "node_modules/is-bigint": { "version": "1.0.4", @@ -9434,9 +5320,9 @@ } }, "node_modules/is-core-module": { - "version": "2.11.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.11.0.tgz", - "integrity": "sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw==", + "version": "2.13.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.0.tgz", + "integrity": "sha512-Z7dk6Qo8pOCp3l4tsX2C5ZVas4V+UxwQodwZhLopL91TX8UyyHEXafPcyoeeWuLrwzHcr3igO78wNLwHJHsMCQ==", "dev": true, "dependencies": { "has": "^1.0.3" @@ -9445,19 +5331,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "dev": true, - "peer": true, - "dependencies": { - "kind-of": "^6.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/is-date-object": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", @@ -9473,58 +5346,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "dev": true, - "peer": true, - "dependencies": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-directory": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/is-directory/-/is-directory-0.3.1.tgz", - "integrity": "sha512-yVChGzahRFvbkscn2MlwGismPO12i9+znNruC5gVEntG3qu0xQMzsGg/JFbrsqDOHtHFPci+V5aP5T9I+yeKqw==", - "dev": true, - "peer": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-docker": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", - "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", - "bin": { - "is-docker": "cli.js" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-extendable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", - "dev": true, - "peer": true, - "dependencies": { - "is-plain-object": "^2.0.4" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -9534,6 +5355,18 @@ "node": ">=0.10.0" } }, + "node_modules/is-finalizationregistry": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.0.2.tgz", + "integrity": "sha512-0by5vtUJs8iFQb5TYUHHPudOR+qXYIMKtiUzvLIZITZUjknFmziyBJuLhVRc+Ds0dREFlskDNJKYIdIzu/9pfw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", @@ -9586,16 +5419,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/is-interactive": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", - "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", - "dev": true, - "peer": true, - "engines": { - "node": ">=8" - } - }, "node_modules/is-map": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.2.tgz", @@ -9621,6 +5444,7 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, "engines": { "node": ">=0.12.0" } @@ -9658,19 +5482,6 @@ "node": ">=8" } }, - "node_modules/is-plain-object": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", - "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", - "dev": true, - "peer": true, - "dependencies": { - "isobject": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/is-regex": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", @@ -9760,16 +5571,12 @@ } }, "node_modules/is-typed-array": { - "version": "1.1.10", - "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.10.tgz", - "integrity": "sha512-PJqgEHiWZvMpaFZ3uTc8kHPM4+4ADTlDniuQL7cU/UDA0Ql7F70yGfHph3cLNe+c9toaigv+DFzTJKhc2CtO6A==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.12.tgz", + "integrity": "sha512-Z14TF2JNG8Lss5/HMqt0//T9JeHXttXy5pH/DBU4vi98ozO2btxzq9MwYDZYnKwU8nRsz/+GVFVRDq3DkVuSPg==", "dev": true, "dependencies": { - "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.2", - "for-each": "^0.3.3", - "gopd": "^1.0.1", - "has-tostringtag": "^1.0.0" + "which-typed-array": "^1.1.11" }, "engines": { "node": ">= 0.4" @@ -9830,26 +5637,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-windows": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", - "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", - "dev": true, - "peer": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-wsl": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-1.1.0.tgz", - "integrity": "sha512-gfygJYZ2gLTDlmbWMI0CE2MwnFzSN/2SZfkMlItC4K/JBlsWVDB0bO6XhqcY13YXE7iMcAJnzTCJjPiTeJJ0Mw==", - "dev": true, - "peer": true, - "engines": { - "node": ">=4" - } - }, "node_modules/isarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", @@ -9859,17 +5646,8 @@ "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" - }, - "node_modules/isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", - "dev": true, - "peer": true, - "engines": { - "node": ">=0.10.0" - } + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", + "dev": true }, "node_modules/isstream": { "version": "0.1.2", @@ -9877,128 +5655,17 @@ "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=", "dev": true }, - "node_modules/jest-get-type": { - "version": "26.3.0", - "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-26.3.0.tgz", - "integrity": "sha512-TpfaviN1R2pQWkIihlfEanwOXK0zcxrKEE4MlU6Tn7keoXdN6/3gK/xl0yEh8DOunn5pOVGKf8hB4R9gVh04ig==", + "node_modules/iterator.prototype": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.2.tgz", + "integrity": "sha512-DR33HMMr8EzwuRL8Y9D3u2BMj8+RqSE850jfGu59kS7tbmPLzGkZmVSfyCFSDxuZiEY6Rzt3T2NA/qU+NwVj1w==", "dev": true, - "peer": true, - "engines": { - "node": ">= 10.14.2" - } - }, - "node_modules/jest-haste-map": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-27.5.1.tgz", - "integrity": "sha512-7GgkZ4Fw4NFbMSDSpZwXeBiIbx+t/46nJ2QitkOjvwPYyZmqttu2TDSimMHP1EkPOi4xUZAN1doE5Vd25H4Jng==", - "dev": true, - "peer": true, "dependencies": { - "@jest/types": "^27.5.1", - "@types/graceful-fs": "^4.1.2", - "@types/node": "*", - "anymatch": "^3.0.3", - "fb-watchman": "^2.0.0", - "graceful-fs": "^4.2.9", - "jest-regex-util": "^27.5.1", - "jest-serializer": "^27.5.1", - "jest-util": "^27.5.1", - "jest-worker": "^27.5.1", - "micromatch": "^4.0.4", - "walker": "^1.0.7" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - }, - "optionalDependencies": { - "fsevents": "^2.3.2" - } - }, - "node_modules/jest-regex-util": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-27.5.1.tgz", - "integrity": "sha512-4bfKq2zie+x16okqDXjXn9ql2B0dScQu+vcwe4TvFVhkVyuWLqpZrZtXxLLWoXYgn0E87I6r6GRYHF7wFZBUvg==", - "dev": true, - "peer": true, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-serializer": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-serializer/-/jest-serializer-27.5.1.tgz", - "integrity": "sha512-jZCyo6iIxO1aqUxpuBlwTDMkzOAJS4a3eYz3YzgxxVQFwLeSA7Jfq5cbqCY+JLvTDrWirgusI/0KwxKMgrdf7w==", - "dev": true, - "peer": true, - "dependencies": { - "@types/node": "*", - "graceful-fs": "^4.2.9" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-util": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-27.5.1.tgz", - "integrity": "sha512-Kv2o/8jNvX1MQ0KGtw480E/w4fBCDOnH6+6DmeKi6LZUIlKA5kwY0YNdlzaWTiVgxqAqik11QyxDOKk543aKXw==", - "dev": true, - "peer": true, - "dependencies": { - "@jest/types": "^27.5.1", - "@types/node": "*", - "chalk": "^4.0.0", - "ci-info": "^3.2.0", - "graceful-fs": "^4.2.9", - "picomatch": "^2.2.3" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-validate": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-26.6.2.tgz", - "integrity": "sha512-NEYZ9Aeyj0i5rQqbq+tpIOom0YS1u2MVu6+euBsvpgIme+FOfRmoC4R5p0JiAUpaFvFy24xgrpMknarR/93XjQ==", - "dev": true, - "peer": true, - "dependencies": { - "@jest/types": "^26.6.2", - "camelcase": "^6.0.0", - "chalk": "^4.0.0", - "jest-get-type": "^26.3.0", - "leven": "^3.1.0", - "pretty-format": "^26.6.2" - }, - "engines": { - "node": ">= 10.14.2" - } - }, - "node_modules/jest-validate/node_modules/@jest/types": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-26.6.2.tgz", - "integrity": "sha512-fC6QCp7Sc5sX6g8Tvbmj4XUTbyrik0akgRy03yjXbQaBWWNWGE7SGtJk98m0N8nzegD/7SggrUlivxo5ax4KWQ==", - "dev": true, - "peer": true, - "dependencies": { - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^15.0.0", - "chalk": "^4.0.0" - }, - "engines": { - "node": ">= 10.14.2" - } - }, - "node_modules/jest-validate/node_modules/@types/yargs": { - "version": "15.0.14", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.14.tgz", - "integrity": "sha512-yEJzHoxf6SyQGhBhIYGXQDSCkJjB6HohDShto7m8vaKg9Yp0Yn8+71J9eakh2bnPg6BfsH9PRMhiRTZnd4eXGQ==", - "dev": true, - "peer": true, - "dependencies": { - "@types/yargs-parser": "*" + "define-properties": "^1.2.1", + "get-intrinsic": "^1.2.1", + "has-symbols": "^1.0.3", + "reflect.getprototypeof": "^1.0.4", + "set-function-name": "^2.0.1" } }, "node_modules/jest-worker": { @@ -10032,18 +5699,6 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, - "node_modules/jetifier": { - "version": "1.6.8", - "resolved": "https://registry.npmjs.org/jetifier/-/jetifier-1.6.8.tgz", - "integrity": "sha512-3Zi16h6L5tXDRQJTb221cnRoVG9/9OvreLdLU2/ZjRv/GILL+2Cemt0IKvkowwkDpvouAU1DQPOJ7qaiHeIdrw==", - "dev": true, - "peer": true, - "bin": { - "jetifier": "bin/jetify", - "jetifier-standalone": "bin/jetifier-standalone", - "jetify": "bin/jetify" - } - }, "node_modules/jmespath": { "version": "0.16.0", "resolved": "https://registry.npmjs.org/jmespath/-/jmespath-0.16.0.tgz", @@ -10053,30 +5708,6 @@ "node": ">= 0.6.0" } }, - "node_modules/joi": { - "version": "17.6.0", - "resolved": "https://registry.npmjs.org/joi/-/joi-17.6.0.tgz", - "integrity": "sha512-OX5dG6DTbcr/kbMFj0KGYxuew69HPcAE3K/sZpEV2nP6e/j/C0HV+HNiBPCASxdx5T7DMoa0s8UeHWMnb6n2zw==", - "dev": true, - "peer": true, - "dependencies": { - "@hapi/hoek": "^9.0.0", - "@hapi/topo": "^5.0.0", - "@sideway/address": "^4.1.3", - "@sideway/formula": "^3.0.0", - "@sideway/pinpoint": "^2.0.0" - } - }, - "node_modules/js-sdsl": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.4.0.tgz", - "integrity": "sha512-FfVSdx6pJ41Oa+CF7RDaFmTnCaFhua+SNYQX74riGOpl96x+2jQCqEfQ2bnXu/5DPCqlRuiqyvTJM0Qjz26IVg==", - "dev": true, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/js-sdsl" - } - }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -10095,224 +5726,12 @@ "js-yaml": "bin/js-yaml.js" } }, - "node_modules/jsan": { - "version": "3.1.14", - "resolved": "https://registry.npmjs.org/jsan/-/jsan-3.1.14.tgz", - "integrity": "sha512-wStfgOJqMv4QKktuH273f5fyi3D3vy2pHOiSDGPvpcS/q+wb/M7AK3vkCcaHbkZxDOlDU/lDJgccygKSG2OhtA==", - "dev": true - }, "node_modules/jsbn": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", "integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==", "dev": true }, - "node_modules/jsc-android": { - "version": "250230.2.1", - "resolved": "https://registry.npmjs.org/jsc-android/-/jsc-android-250230.2.1.tgz", - "integrity": "sha512-KmxeBlRjwoqCnBBKGsihFtvsBHyUFlBxJPK4FzeYcIuBfdjv6jFys44JITAgSTbQD+vIdwMEfyZklsuQX0yI1Q==", - "dev": true, - "peer": true - }, - "node_modules/jscodeshift": { - "version": "0.13.1", - "resolved": "https://registry.npmjs.org/jscodeshift/-/jscodeshift-0.13.1.tgz", - "integrity": "sha512-lGyiEbGOvmMRKgWk4vf+lUrCWO/8YR8sUR3FKF1Cq5fovjZDlIcw3Hu5ppLHAnEXshVffvaM0eyuY/AbOeYpnQ==", - "dev": true, - "peer": true, - "dependencies": { - "@babel/core": "^7.13.16", - "@babel/parser": "^7.13.16", - "@babel/plugin-proposal-class-properties": "^7.13.0", - "@babel/plugin-proposal-nullish-coalescing-operator": "^7.13.8", - "@babel/plugin-proposal-optional-chaining": "^7.13.12", - "@babel/plugin-transform-modules-commonjs": "^7.13.8", - "@babel/preset-flow": "^7.13.13", - "@babel/preset-typescript": "^7.13.0", - "@babel/register": "^7.13.16", - "babel-core": "^7.0.0-bridge.0", - "chalk": "^4.1.2", - "flow-parser": "0.*", - "graceful-fs": "^4.2.4", - "micromatch": "^3.1.10", - "neo-async": "^2.5.0", - "node-dir": "^0.1.17", - "recast": "^0.20.4", - "temp": "^0.8.4", - "write-file-atomic": "^2.3.0" - }, - "bin": { - "jscodeshift": "bin/jscodeshift.js" - }, - "peerDependencies": { - "@babel/preset-env": "^7.1.6" - } - }, - "node_modules/jscodeshift/node_modules/braces": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", - "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", - "dev": true, - "peer": true, - "dependencies": { - "arr-flatten": "^1.1.0", - "array-unique": "^0.3.2", - "extend-shallow": "^2.0.1", - "fill-range": "^4.0.0", - "isobject": "^3.0.1", - "repeat-element": "^1.1.2", - "snapdragon": "^0.8.1", - "snapdragon-node": "^2.0.1", - "split-string": "^3.0.2", - "to-regex": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/jscodeshift/node_modules/braces/node_modules/extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", - "dev": true, - "peer": true, - "dependencies": { - "is-extendable": "^0.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/jscodeshift/node_modules/fill-range": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", - "integrity": "sha512-VcpLTWqWDiTerugjj8e3+esbg+skS3M9e54UuR3iCeIDMXCLTsAH8hTSzDQU/X6/6t3eYkOKoZSef2PlU6U1XQ==", - "dev": true, - "peer": true, - "dependencies": { - "extend-shallow": "^2.0.1", - "is-number": "^3.0.0", - "repeat-string": "^1.6.1", - "to-regex-range": "^2.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/jscodeshift/node_modules/fill-range/node_modules/extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", - "dev": true, - "peer": true, - "dependencies": { - "is-extendable": "^0.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/jscodeshift/node_modules/is-extendable": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", - "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", - "dev": true, - "peer": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/jscodeshift/node_modules/is-number": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", - "integrity": "sha512-4cboCqIpliH+mAvFNegjZQ4kgKc3ZUhQVr3HvWbSh5q3WH2v82ct+T2Y1hdU5Gdtorx/cLifQjqCbL7bpznLTg==", - "dev": true, - "peer": true, - "dependencies": { - "kind-of": "^3.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/jscodeshift/node_modules/is-number/node_modules/kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", - "dev": true, - "peer": true, - "dependencies": { - "is-buffer": "^1.1.5" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/jscodeshift/node_modules/micromatch": { - "version": "3.1.10", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", - "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", - "dev": true, - "peer": true, - "dependencies": { - "arr-diff": "^4.0.0", - "array-unique": "^0.3.2", - "braces": "^2.3.1", - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "extglob": "^2.0.4", - "fragment-cache": "^0.2.1", - "kind-of": "^6.0.2", - "nanomatch": "^1.2.9", - "object.pick": "^1.3.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/jscodeshift/node_modules/rimraf": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", - "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", - "dev": true, - "peer": true, - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - } - }, - "node_modules/jscodeshift/node_modules/temp": { - "version": "0.8.4", - "resolved": "https://registry.npmjs.org/temp/-/temp-0.8.4.tgz", - "integrity": "sha512-s0ZZzd0BzYv5tLSptZooSjK8oj6C+c19p7Vqta9+6NPOf7r+fxq0cJe6/oN4LTC79sy5NY8ucOJNgwsKCSbfqg==", - "dev": true, - "peer": true, - "dependencies": { - "rimraf": "~2.6.2" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/jscodeshift/node_modules/to-regex-range": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", - "integrity": "sha512-ZZWNfCjUokXXDGXFpZehJIkZqq91BcULFq/Pi7M5i4JnxXdhMKAK682z8bCW3o8Hj1wuuzoKcW3DfVzaP6VuNg==", - "dev": true, - "peer": true, - "dependencies": { - "is-number": "^3.0.0", - "repeat-string": "^1.6.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/jsesc": { "version": "2.5.2", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", @@ -10326,13 +5745,6 @@ "node": ">=4" } }, - "node_modules/json-parse-better-errors": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", - "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", - "dev": true, - "peer": true - }, "node_modules/json-parse-even-better-errors": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", @@ -10381,6 +5793,7 @@ "version": "6.1.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dev": true, "dependencies": { "universalify": "^2.0.0" }, @@ -10416,52 +5829,14 @@ "node": ">=4.0" } }, - "node_modules/kind-of": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", - "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", - "dev": true, - "peer": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/klaw": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/klaw/-/klaw-1.3.1.tgz", - "integrity": "sha512-TED5xi9gGQjGpNnvRWknrwAB1eL5GciPfVFOt3Vk1OJCVDQbzuSfrF3hkUQKlsgKrG1F+0t5W0m+Fje1jIt8rw==", - "dev": true, - "peer": true, - "optionalDependencies": { - "graceful-fs": "^4.1.9" - } - }, - "node_modules/klaw-sync": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/klaw-sync/-/klaw-sync-6.0.0.tgz", - "integrity": "sha512-nIeuVSzdCCs6TDPTqI8w1Yre34sSq7AkZ4B3sfOBbI2CgVSB4Du4aLQijFU2+lhAFCwt9+42Hel6lQNIv6AntQ==", - "dependencies": { - "graceful-fs": "^4.1.11" - } - }, - "node_modules/kleur": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", - "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", - "dev": true, - "peer": true, - "engines": { - "node": ">=6" - } - }, "node_modules/knex": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/knex/-/knex-2.4.2.tgz", - "integrity": "sha512-tMI1M7a+xwHhPxjbl/H9K1kHX+VncEYcvCx5K00M16bWvpYPKAZd6QrCu68PtHAdIZNQPWZn0GVhqVBEthGWCg==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/knex/-/knex-2.5.1.tgz", + "integrity": "sha512-z78DgGKUr4SE/6cm7ku+jHvFT0X97aERh/f0MUKAKgFnwCYBEW4TFBqtHWFYiJFid7fMrtpZ/gxJthvz5mEByA==", "dev": true, "dependencies": { "colorette": "2.0.19", - "commander": "^9.1.0", + "commander": "^10.0.0", "debug": "4.3.4", "escalade": "^3.1.1", "esm": "^3.2.25", @@ -10469,7 +5844,7 @@ "getopts": "2.3.0", "interpret": "^2.2.0", "lodash": "^4.17.21", - "pg-connection-string": "2.5.0", + "pg-connection-string": "2.6.1", "rechoir": "^0.8.0", "resolve-from": "^5.0.0", "tarn": "^3.0.2", @@ -10512,12 +5887,12 @@ "dev": true }, "node_modules/knex/node_modules/commander": { - "version": "9.4.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-9.4.1.tgz", - "integrity": "sha512-5EEkTNyHNGFPD2H+c/dXXfQZYa/scCKasxWcXJaWnNJ99pnQN9Vnmqow+p+PlFPE63Q6mThaZws1T+HxfpgtPw==", + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", + "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", "dev": true, "engines": { - "node": "^12.20.0 || >=14" + "node": ">=14" } }, "node_modules/knex/node_modules/rechoir": { @@ -10565,16 +5940,6 @@ "node": "> 0.8" } }, - "node_modules/leven": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", - "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", - "dev": true, - "peer": true, - "engines": { - "node": ">=6" - } - }, "node_modules/levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -10597,12 +5962,6 @@ "immediate": "~3.0.5" } }, - "node_modules/linked-list": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/linked-list/-/linked-list-0.1.0.tgz", - "integrity": "sha512-Zr4ovrd0ODzF3ut2TWZMdHIxb8iFdJc/P3QM4iCJdlxxGHXo69c9hGIHzLo8/FtuR9E6WUZc5irKhtPUgOKMAg==", - "dev": true - }, "node_modules/listr2": { "version": "3.10.0", "resolved": "https://registry.npmjs.org/listr2/-/listr2-3.10.0.tgz", @@ -10664,19 +6023,6 @@ "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", "dev": true }, - "node_modules/lodash-es": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", - "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==", - "dev": true - }, - "node_modules/lodash.debounce": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", - "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", - "dev": true, - "peer": true - }, "node_modules/lodash.intersection": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/lodash.intersection/-/lodash.intersection-4.4.0.tgz", @@ -10725,13 +6071,6 @@ "integrity": "sha1-DdOXEhPHxW34gJd9UEyI+0cal6w=", "dev": true }, - "node_modules/lodash.throttle": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/lodash.throttle/-/lodash.throttle-4.1.1.tgz", - "integrity": "sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ==", - "dev": true, - "peer": true - }, "node_modules/lodash.without": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/lodash.without/-/lodash.without-4.4.0.tgz", @@ -10821,181 +6160,6 @@ "node": ">=8" } }, - "node_modules/logkitty": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/logkitty/-/logkitty-0.7.1.tgz", - "integrity": "sha512-/3ER20CTTbahrCrpYfPn7Xavv9diBROZpoXGVZDWMw4b/X4uuUwAC0ki85tgsdMRONURyIJbcOvS94QsUBYPbQ==", - "dev": true, - "peer": true, - "dependencies": { - "ansi-fragments": "^0.2.1", - "dayjs": "^1.8.15", - "yargs": "^15.1.0" - }, - "bin": { - "logkitty": "bin/logkitty.js" - } - }, - "node_modules/logkitty/node_modules/camelcase": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", - "dev": true, - "peer": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/logkitty/node_modules/cliui": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", - "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", - "dev": true, - "peer": true, - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", - "wrap-ansi": "^6.2.0" - } - }, - "node_modules/logkitty/node_modules/decamelize": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", - "dev": true, - "peer": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/logkitty/node_modules/find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, - "peer": true, - "dependencies": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/logkitty/node_modules/locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dev": true, - "peer": true, - "dependencies": { - "p-locate": "^4.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/logkitty/node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dev": true, - "peer": true, - "dependencies": { - "p-try": "^2.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/logkitty/node_modules/p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dev": true, - "peer": true, - "dependencies": { - "p-limit": "^2.2.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/logkitty/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "peer": true, - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/logkitty/node_modules/wrap-ansi": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", - "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", - "dev": true, - "peer": true, - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/logkitty/node_modules/y18n": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", - "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", - "dev": true, - "peer": true - }, - "node_modules/logkitty/node_modules/yargs": { - "version": "15.4.1", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", - "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", - "dev": true, - "peer": true, - "dependencies": { - "cliui": "^6.0.0", - "decamelize": "^1.2.0", - "find-up": "^4.1.0", - "get-caller-file": "^2.0.1", - "require-directory": "^2.1.1", - "require-main-filename": "^2.0.0", - "set-blocking": "^2.0.0", - "string-width": "^4.2.0", - "which-module": "^2.0.0", - "y18n": "^4.0.0", - "yargs-parser": "^18.1.2" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/logkitty/node_modules/yargs-parser": { - "version": "18.1.3", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", - "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", - "dev": true, - "peer": true, - "dependencies": { - "camelcase": "^5.0.0", - "decamelize": "^1.2.0" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -11009,9 +6173,9 @@ } }, "node_modules/loupe": { - "version": "2.3.4", - "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.4.tgz", - "integrity": "sha512-OvKfgCC2Ndby6aSTREl5aCCPTNIzlDfQZvZxNUrBrihDhL3xcrYegTblhmEiCrg2kKQz4XsFIaemE5BF4ybSaQ==", + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.6.tgz", + "integrity": "sha512-RaPMZKiMy8/JruncMU5Bt6na1eftNoo++R4Y+N2FrxkDVTrGvcyzFTsaGif4QTeKESheMGegbhw6iUAq+5A8zA==", "dev": true, "dependencies": { "get-func-name": "^2.0.0" @@ -11030,48 +6194,14 @@ } }, "node_modules/lz-string": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.4.4.tgz", - "integrity": "sha512-0ckx7ZHRPqb0oUm8zNr+90mtf9DQB60H1wMCjBtfi62Kl3a7JbHob6gA2bC+xRvZoOL+1hzUK8jeuEIQE8svEQ==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", "dev": true, "bin": { "lz-string": "bin/bin.js" } }, - "node_modules/make-dir": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", - "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", - "dev": true, - "peer": true, - "dependencies": { - "pify": "^4.0.1", - "semver": "^5.6.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/make-dir/node_modules/pify": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", - "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", - "dev": true, - "peer": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/make-dir/node_modules/semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true, - "peer": true, - "bin": { - "semver": "bin/semver" - } - }, "node_modules/make-error": { "version": "1.3.6", "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", @@ -11087,88 +6217,6 @@ "make-error": "^1.3.5" } }, - "node_modules/makeerror": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", - "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", - "dev": true, - "peer": true, - "dependencies": { - "tmpl": "1.0.5" - } - }, - "node_modules/map-cache": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", - "integrity": "sha512-8y/eV9QQZCiyn1SprXSrCmqJN0yNRATe+PO8ztwqrvrbdRLA3eYJF0yaR0YayLWkMbsQSKWS9N2gPcGEc4UsZg==", - "dev": true, - "peer": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/map-visit": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/map-visit/-/map-visit-1.0.0.tgz", - "integrity": "sha512-4y7uGv8bd2WdM9vpQsiQNo41Ln1NvhvDRuVt0k2JZQ+ezN2uaQes7lZeZ+QQUHOLQAtDaBJ+7wCbi+ab/KFs+w==", - "dev": true, - "peer": true, - "dependencies": { - "object-visit": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/mattermost-redux": { - "version": "5.33.1", - "resolved": "https://registry.npmjs.org/mattermost-redux/-/mattermost-redux-5.33.1.tgz", - "integrity": "sha512-fHTW5PeB0xAE5u6uMds+hlWaJsUeWgE1034vekUah1X4bZCzBcMK4fM+ycGaIQ7c5Ylj9ML6Ni10XJYiQpMo5w==", - "dev": true, - "dependencies": { - "core-js": "3.8.3", - "form-data": "3.0.0", - "gfycat-sdk": "1.4.18", - "moment-timezone": "0.5.32", - "redux": "4.0.5", - "redux-action-buffer": "1.2.0", - "redux-offline": "git+https://github.com/enahum/redux-offline.git#885024de96b6ec73650c340c8928066585c413df", - "redux-persist": "4.9.1", - "redux-persist-node-storage": "2.0.0", - "redux-thunk": "2.3.0", - "remote-redux-devtools": "0.5.16", - "reselect": "4.0.0", - "rudder-sdk-js": "1.0.14", - "serialize-error": "6.0.0", - "shallow-equals": "1.0.0" - } - }, - "node_modules/mattermost-redux/node_modules/form-data": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.0.tgz", - "integrity": "sha512-CKMFDglpbMi6PyN+brwB9Q/GOw0eAnsrEZDgcsH5Krhz5Od/haKHAX0NmQfha2zPPz0JpWzA7GJHGSnvCRLWsg==", - "dev": true, - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/mattermost-redux/node_modules/moment-timezone": { - "version": "0.5.32", - "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.32.tgz", - "integrity": "sha512-Z8QNyuQHQAmWucp8Knmgei8YNo28aLjJq6Ma+jy1ZSpSk5nyfRT8xgUbSQvD2+2UajISfenndwvFuH3NGS+nvA==", - "dev": true, - "dependencies": { - "moment": ">= 2.9.0" - }, - "engines": { - "node": "*" - } - }, "node_modules/md5": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/md5/-/md5-2.3.0.tgz", @@ -11189,13 +6237,6 @@ "node": ">= 0.6" } }, - "node_modules/memoize-one": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz", - "integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==", - "dev": true, - "peer": true - }, "node_modules/memory-fs": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.2.0.tgz", @@ -11232,794 +6273,11 @@ "node": ">= 0.6" } }, - "node_modules/metro": { - "version": "0.70.3", - "resolved": "https://registry.npmjs.org/metro/-/metro-0.70.3.tgz", - "integrity": "sha512-uEWS7xg8oTetQDABYNtsyeUjdLhH3KAvLFpaFFoJqUpOk2A3iygszdqmjobFl6W4zrvKDJS+XxdMR1roYvUhTw==", - "dev": true, - "peer": true, - "dependencies": { - "@babel/code-frame": "^7.0.0", - "@babel/core": "^7.14.0", - "@babel/generator": "^7.14.0", - "@babel/parser": "^7.14.0", - "@babel/template": "^7.0.0", - "@babel/traverse": "^7.14.0", - "@babel/types": "^7.0.0", - "absolute-path": "^0.0.0", - "accepts": "^1.3.7", - "async": "^3.2.2", - "chalk": "^4.0.0", - "ci-info": "^2.0.0", - "connect": "^3.6.5", - "debug": "^2.2.0", - "denodeify": "^1.2.1", - "error-stack-parser": "^2.0.6", - "fs-extra": "^1.0.0", - "graceful-fs": "^4.2.4", - "hermes-parser": "0.6.0", - "image-size": "^0.6.0", - "invariant": "^2.2.4", - "jest-haste-map": "^27.3.1", - "jest-worker": "^27.2.0", - "lodash.throttle": "^4.1.1", - "metro-babel-transformer": "0.70.3", - "metro-cache": "0.70.3", - "metro-cache-key": "0.70.3", - "metro-config": "0.70.3", - "metro-core": "0.70.3", - "metro-hermes-compiler": "0.70.3", - "metro-inspector-proxy": "0.70.3", - "metro-minify-uglify": "0.70.3", - "metro-react-native-babel-preset": "0.70.3", - "metro-resolver": "0.70.3", - "metro-runtime": "0.70.3", - "metro-source-map": "0.70.3", - "metro-symbolicate": "0.70.3", - "metro-transform-plugins": "0.70.3", - "metro-transform-worker": "0.70.3", - "mime-types": "^2.1.27", - "node-fetch": "^2.2.0", - "nullthrows": "^1.1.1", - "rimraf": "^2.5.4", - "serialize-error": "^2.1.0", - "source-map": "^0.5.6", - "strip-ansi": "^6.0.0", - "temp": "0.8.3", - "throat": "^5.0.0", - "ws": "^7.5.1", - "yargs": "^15.3.1" - }, - "bin": { - "metro": "src/cli.js" - } - }, - "node_modules/metro-babel-transformer": { - "version": "0.70.3", - "resolved": "https://registry.npmjs.org/metro-babel-transformer/-/metro-babel-transformer-0.70.3.tgz", - "integrity": "sha512-bWhZRMn+mIOR/s3BDpFevWScz9sV8FGktVfMlF1eJBLoX24itHDbXvTktKBYi38PWIKcHedh6THSFpJogfuwNA==", - "dev": true, - "peer": true, - "dependencies": { - "@babel/core": "^7.14.0", - "hermes-parser": "0.6.0", - "metro-source-map": "0.70.3", - "nullthrows": "^1.1.1" - } - }, - "node_modules/metro-cache": { - "version": "0.70.3", - "resolved": "https://registry.npmjs.org/metro-cache/-/metro-cache-0.70.3.tgz", - "integrity": "sha512-iCix/+z812fUqa6KlOxaTkY6LQQDoXIe/VljXkGIvpygSCmYyhjQpfQVZEVVPezFmUBYXNdabdQ6cYx6JX3yMg==", - "dev": true, - "peer": true, - "dependencies": { - "metro-core": "0.70.3", - "rimraf": "^2.5.4" - } - }, - "node_modules/metro-cache-key": { - "version": "0.70.3", - "resolved": "https://registry.npmjs.org/metro-cache-key/-/metro-cache-key-0.70.3.tgz", - "integrity": "sha512-0zpw+IcpM3hmGd5sKMdxNv3sbOIUYnMUvx1/yaM6vNRReSPmOLX0bP8fYf3CGgk8NEreZ1OHbVsuw7bdKt40Mw==", - "dev": true, - "peer": true - }, - "node_modules/metro-config": { - "version": "0.70.3", - "resolved": "https://registry.npmjs.org/metro-config/-/metro-config-0.70.3.tgz", - "integrity": "sha512-SSCDjSTygoCgzoj61DdrBeJzZDRwQxUEfcgc6t6coxWSExXNR4mOngz0q4SAam49Bmjq9J2Jft6qUKnUTPrRgA==", - "dev": true, - "peer": true, - "dependencies": { - "cosmiconfig": "^5.0.5", - "jest-validate": "^26.5.2", - "metro": "0.70.3", - "metro-cache": "0.70.3", - "metro-core": "0.70.3", - "metro-runtime": "0.70.3" - } - }, - "node_modules/metro-core": { - "version": "0.70.3", - "resolved": "https://registry.npmjs.org/metro-core/-/metro-core-0.70.3.tgz", - "integrity": "sha512-NzfHB/w5R7yLaOeU1tzPTbBzCRsYSvpKJkLMP0yudszKZzIAZqNdjoEJ9GZ688Wi0ynZxcU0BxukXh4my80ZBw==", - "dev": true, - "peer": true, - "dependencies": { - "jest-haste-map": "^27.3.1", - "lodash.throttle": "^4.1.1", - "metro-resolver": "0.70.3" - } - }, - "node_modules/metro-hermes-compiler": { - "version": "0.70.3", - "resolved": "https://registry.npmjs.org/metro-hermes-compiler/-/metro-hermes-compiler-0.70.3.tgz", - "integrity": "sha512-W6WttLi4E72JL/NyteQ84uxYOFMibe0PUr9aBKuJxxfCq6QRnJKOVcNY0NLW0He2tneXGk+8ZsNz8c0flEvYqg==", - "dev": true, - "peer": true - }, - "node_modules/metro-inspector-proxy": { - "version": "0.70.3", - "resolved": "https://registry.npmjs.org/metro-inspector-proxy/-/metro-inspector-proxy-0.70.3.tgz", - "integrity": "sha512-qQoNdPGrmyoJSWYkxSDpTaAI8xyqVdNDVVj9KRm1PG8niSuYmrCCFGLLFsMvkVYwsCWUGHoGBx0UoAzVp14ejw==", - "dev": true, - "peer": true, - "dependencies": { - "connect": "^3.6.5", - "debug": "^2.2.0", - "ws": "^7.5.1", - "yargs": "^15.3.1" - }, - "bin": { - "metro-inspector-proxy": "src/cli.js" - } - }, - "node_modules/metro-inspector-proxy/node_modules/camelcase": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", - "dev": true, - "peer": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/metro-inspector-proxy/node_modules/cliui": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", - "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", - "dev": true, - "peer": true, - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", - "wrap-ansi": "^6.2.0" - } - }, - "node_modules/metro-inspector-proxy/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "peer": true, - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/metro-inspector-proxy/node_modules/decamelize": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", - "dev": true, - "peer": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/metro-inspector-proxy/node_modules/find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, - "peer": true, - "dependencies": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/metro-inspector-proxy/node_modules/locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dev": true, - "peer": true, - "dependencies": { - "p-locate": "^4.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/metro-inspector-proxy/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true, - "peer": true - }, - "node_modules/metro-inspector-proxy/node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dev": true, - "peer": true, - "dependencies": { - "p-try": "^2.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/metro-inspector-proxy/node_modules/p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dev": true, - "peer": true, - "dependencies": { - "p-limit": "^2.2.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/metro-inspector-proxy/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "peer": true, - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/metro-inspector-proxy/node_modules/wrap-ansi": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", - "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", - "dev": true, - "peer": true, - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/metro-inspector-proxy/node_modules/ws": { - "version": "7.5.8", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.8.tgz", - "integrity": "sha512-ri1Id1WinAX5Jqn9HejiGb8crfRio0Qgu8+MtL36rlTA6RLsMdWt1Az/19A2Qij6uSHUMphEFaTKa4WG+UNHNw==", - "dev": true, - "peer": true, - "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 - } - } - }, - "node_modules/metro-inspector-proxy/node_modules/y18n": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", - "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", - "dev": true, - "peer": true - }, - "node_modules/metro-inspector-proxy/node_modules/yargs": { - "version": "15.4.1", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", - "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", - "dev": true, - "peer": true, - "dependencies": { - "cliui": "^6.0.0", - "decamelize": "^1.2.0", - "find-up": "^4.1.0", - "get-caller-file": "^2.0.1", - "require-directory": "^2.1.1", - "require-main-filename": "^2.0.0", - "set-blocking": "^2.0.0", - "string-width": "^4.2.0", - "which-module": "^2.0.0", - "y18n": "^4.0.0", - "yargs-parser": "^18.1.2" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/metro-inspector-proxy/node_modules/yargs-parser": { - "version": "18.1.3", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", - "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", - "dev": true, - "peer": true, - "dependencies": { - "camelcase": "^5.0.0", - "decamelize": "^1.2.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/metro-minify-uglify": { - "version": "0.70.3", - "resolved": "https://registry.npmjs.org/metro-minify-uglify/-/metro-minify-uglify-0.70.3.tgz", - "integrity": "sha512-oHyjV9WDqOlDE1FPtvs6tIjjeY/oP1PNUPYL1wqyYtqvjN+zzAOrcbsAAL1sv+WARaeiMsWkF2bwtNo+Hghoog==", - "dev": true, - "peer": true, - "dependencies": { - "uglify-es": "^3.1.9" - } - }, - "node_modules/metro-react-native-babel-preset": { - "version": "0.70.3", - "resolved": "https://registry.npmjs.org/metro-react-native-babel-preset/-/metro-react-native-babel-preset-0.70.3.tgz", - "integrity": "sha512-4Nxc1zEiHEu+GTdEMEsHnRgfaBkg8f/Td3+FcQ8NTSvs+xL3LBrQy6N07idWSQZHIdGFf+tTHvRfSIWLD8u8Tg==", - "dev": true, - "peer": true, - "dependencies": { - "@babel/core": "^7.14.0", - "@babel/plugin-proposal-async-generator-functions": "^7.0.0", - "@babel/plugin-proposal-class-properties": "^7.0.0", - "@babel/plugin-proposal-export-default-from": "^7.0.0", - "@babel/plugin-proposal-nullish-coalescing-operator": "^7.0.0", - "@babel/plugin-proposal-object-rest-spread": "^7.0.0", - "@babel/plugin-proposal-optional-catch-binding": "^7.0.0", - "@babel/plugin-proposal-optional-chaining": "^7.0.0", - "@babel/plugin-syntax-dynamic-import": "^7.0.0", - "@babel/plugin-syntax-export-default-from": "^7.0.0", - "@babel/plugin-syntax-flow": "^7.2.0", - "@babel/plugin-syntax-nullish-coalescing-operator": "^7.0.0", - "@babel/plugin-syntax-optional-chaining": "^7.0.0", - "@babel/plugin-transform-arrow-functions": "^7.0.0", - "@babel/plugin-transform-async-to-generator": "^7.0.0", - "@babel/plugin-transform-block-scoping": "^7.0.0", - "@babel/plugin-transform-classes": "^7.0.0", - "@babel/plugin-transform-computed-properties": "^7.0.0", - "@babel/plugin-transform-destructuring": "^7.0.0", - "@babel/plugin-transform-exponentiation-operator": "^7.0.0", - "@babel/plugin-transform-flow-strip-types": "^7.0.0", - "@babel/plugin-transform-function-name": "^7.0.0", - "@babel/plugin-transform-literals": "^7.0.0", - "@babel/plugin-transform-modules-commonjs": "^7.0.0", - "@babel/plugin-transform-named-capturing-groups-regex": "^7.0.0", - "@babel/plugin-transform-parameters": "^7.0.0", - "@babel/plugin-transform-react-display-name": "^7.0.0", - "@babel/plugin-transform-react-jsx": "^7.0.0", - "@babel/plugin-transform-react-jsx-self": "^7.0.0", - "@babel/plugin-transform-react-jsx-source": "^7.0.0", - "@babel/plugin-transform-runtime": "^7.0.0", - "@babel/plugin-transform-shorthand-properties": "^7.0.0", - "@babel/plugin-transform-spread": "^7.0.0", - "@babel/plugin-transform-sticky-regex": "^7.0.0", - "@babel/plugin-transform-template-literals": "^7.0.0", - "@babel/plugin-transform-typescript": "^7.5.0", - "@babel/plugin-transform-unicode-regex": "^7.0.0", - "@babel/template": "^7.0.0", - "react-refresh": "^0.4.0" - }, - "peerDependencies": { - "@babel/core": "*" - } - }, - "node_modules/metro-react-native-babel-transformer": { - "version": "0.70.3", - "resolved": "https://registry.npmjs.org/metro-react-native-babel-transformer/-/metro-react-native-babel-transformer-0.70.3.tgz", - "integrity": "sha512-WKBU6S/G50j9cfmFM4k4oRYprd8u3qjleD4so1E2zbTNILg+gYla7ZFGCAvi2G0ZcqS2XuGCR375c2hF6VVvwg==", - "dev": true, - "peer": true, - "dependencies": { - "@babel/core": "^7.14.0", - "babel-preset-fbjs": "^3.4.0", - "hermes-parser": "0.6.0", - "metro-babel-transformer": "0.70.3", - "metro-react-native-babel-preset": "0.70.3", - "metro-source-map": "0.70.3", - "nullthrows": "^1.1.1" - }, - "peerDependencies": { - "@babel/core": "*" - } - }, - "node_modules/metro-resolver": { - "version": "0.70.3", - "resolved": "https://registry.npmjs.org/metro-resolver/-/metro-resolver-0.70.3.tgz", - "integrity": "sha512-5Pc5S/Gs4RlLbziuIWtvtFd9GRoILlaRC8RZDVq5JZWcWHywKy/PjNmOBNhpyvtRlzpJfy/ssIfLhu8zINt1Mw==", - "dev": true, - "peer": true, - "dependencies": { - "absolute-path": "^0.0.0" - } - }, - "node_modules/metro-runtime": { - "version": "0.70.3", - "resolved": "https://registry.npmjs.org/metro-runtime/-/metro-runtime-0.70.3.tgz", - "integrity": "sha512-22xU7UdXZacniTIDZgN2EYtmfau2pPyh97Dcs+cWrLcJYgfMKjWBtesnDcUAQy3PHekDYvBdJZkoQUeskYTM+w==", - "dev": true, - "peer": true, - "dependencies": { - "@babel/runtime": "^7.0.0" - } - }, - "node_modules/metro-source-map": { - "version": "0.70.3", - "resolved": "https://registry.npmjs.org/metro-source-map/-/metro-source-map-0.70.3.tgz", - "integrity": "sha512-zsYtZGrwRbbGEFHtmMqqeCH9K9aTGNVPsurMOWCUeQA3VGyVGXPGtLMC+CdAM9jLpUyg6jw2xh0esxi+tYH7Uw==", - "dev": true, - "peer": true, - "dependencies": { - "@babel/traverse": "^7.14.0", - "@babel/types": "^7.0.0", - "invariant": "^2.2.4", - "metro-symbolicate": "0.70.3", - "nullthrows": "^1.1.1", - "ob1": "0.70.3", - "source-map": "^0.5.6", - "vlq": "^1.0.0" - } - }, - "node_modules/metro-source-map/node_modules/source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", - "dev": true, - "peer": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/metro-symbolicate": { - "version": "0.70.3", - "resolved": "https://registry.npmjs.org/metro-symbolicate/-/metro-symbolicate-0.70.3.tgz", - "integrity": "sha512-JTYkF1dpeDUssQ84juE1ycnhHki2ylJBBdJE1JHtfu5oC+z1ElDbBdPHq90Uvt8HbRov/ZAnxvv7Zy6asS+WCA==", - "dev": true, - "peer": true, - "dependencies": { - "invariant": "^2.2.4", - "metro-source-map": "0.70.3", - "nullthrows": "^1.1.1", - "source-map": "^0.5.6", - "through2": "^2.0.1", - "vlq": "^1.0.0" - }, - "bin": { - "metro-symbolicate": "src/index.js" - }, - "engines": { - "node": ">=8.3" - } - }, - "node_modules/metro-symbolicate/node_modules/source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", - "dev": true, - "peer": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/metro-transform-plugins": { - "version": "0.70.3", - "resolved": "https://registry.npmjs.org/metro-transform-plugins/-/metro-transform-plugins-0.70.3.tgz", - "integrity": "sha512-dQRIJoTkWZN2IVS2KzgS1hs7ZdHDX3fS3esfifPkqFAEwHiLctCf0EsPgIknp0AjMLvmGWfSLJigdRB/dc0ASw==", - "dev": true, - "peer": true, - "dependencies": { - "@babel/core": "^7.14.0", - "@babel/generator": "^7.14.0", - "@babel/template": "^7.0.0", - "@babel/traverse": "^7.14.0", - "nullthrows": "^1.1.1" - } - }, - "node_modules/metro-transform-worker": { - "version": "0.70.3", - "resolved": "https://registry.npmjs.org/metro-transform-worker/-/metro-transform-worker-0.70.3.tgz", - "integrity": "sha512-MtVVsnHhhBOp9GRLCdAb2mD1dTCsIzT4+m34KMRdBDCEbDIb90YafT5prpU8qbj5uKd0o2FOQdrJ5iy5zQilHw==", - "dev": true, - "peer": true, - "dependencies": { - "@babel/core": "^7.14.0", - "@babel/generator": "^7.14.0", - "@babel/parser": "^7.14.0", - "@babel/types": "^7.0.0", - "babel-preset-fbjs": "^3.4.0", - "metro": "0.70.3", - "metro-babel-transformer": "0.70.3", - "metro-cache": "0.70.3", - "metro-cache-key": "0.70.3", - "metro-hermes-compiler": "0.70.3", - "metro-source-map": "0.70.3", - "metro-transform-plugins": "0.70.3", - "nullthrows": "^1.1.1" - } - }, - "node_modules/metro/node_modules/camelcase": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", - "dev": true, - "peer": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/metro/node_modules/ci-info": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz", - "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==", - "dev": true, - "peer": true - }, - "node_modules/metro/node_modules/cliui": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", - "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", - "dev": true, - "peer": true, - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", - "wrap-ansi": "^6.2.0" - } - }, - "node_modules/metro/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "peer": true, - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/metro/node_modules/decamelize": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", - "dev": true, - "peer": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/metro/node_modules/find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, - "peer": true, - "dependencies": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/metro/node_modules/fs-extra": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-1.0.0.tgz", - "integrity": "sha512-VerQV6vEKuhDWD2HGOybV6v5I73syoc/cXAbKlgTC7M/oFVEtklWlp9QH2Ijw3IaWDOQcMkldSPa7zXy79Z/UQ==", - "dev": true, - "peer": true, - "dependencies": { - "graceful-fs": "^4.1.2", - "jsonfile": "^2.1.0", - "klaw": "^1.0.0" - } - }, - "node_modules/metro/node_modules/jsonfile": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-2.4.0.tgz", - "integrity": "sha512-PKllAqbgLgxHaj8TElYymKCAgrASebJrWpTnEkOaTowt23VKXXN0sUeriJ+eh7y6ufb/CC5ap11pz71/cM0hUw==", - "dev": true, - "peer": true, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/metro/node_modules/locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dev": true, - "peer": true, - "dependencies": { - "p-locate": "^4.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/metro/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true, - "peer": true - }, - "node_modules/metro/node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dev": true, - "peer": true, - "dependencies": { - "p-try": "^2.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/metro/node_modules/p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dev": true, - "peer": true, - "dependencies": { - "p-limit": "^2.2.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/metro/node_modules/serialize-error": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-2.1.0.tgz", - "integrity": "sha512-ghgmKt5o4Tly5yEG/UJp8qTd0AN7Xalw4XBtDEKP655B699qMEtra1WlXeE6WIvdEG481JvRxULKsInq/iNysw==", - "dev": true, - "peer": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/metro/node_modules/source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", - "dev": true, - "peer": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/metro/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "peer": true, - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/metro/node_modules/wrap-ansi": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", - "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", - "dev": true, - "peer": true, - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/metro/node_modules/ws": { - "version": "7.5.8", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.8.tgz", - "integrity": "sha512-ri1Id1WinAX5Jqn9HejiGb8crfRio0Qgu8+MtL36rlTA6RLsMdWt1Az/19A2Qij6uSHUMphEFaTKa4WG+UNHNw==", - "dev": true, - "peer": true, - "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 - } - } - }, - "node_modules/metro/node_modules/y18n": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", - "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", - "dev": true, - "peer": true - }, - "node_modules/metro/node_modules/yargs": { - "version": "15.4.1", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", - "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", - "dev": true, - "peer": true, - "dependencies": { - "cliui": "^6.0.0", - "decamelize": "^1.2.0", - "find-up": "^4.1.0", - "get-caller-file": "^2.0.1", - "require-directory": "^2.1.1", - "require-main-filename": "^2.0.0", - "set-blocking": "^2.0.0", - "string-width": "^4.2.0", - "which-module": "^2.0.0", - "y18n": "^4.0.0", - "yargs-parser": "^18.1.2" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/metro/node_modules/yargs-parser": { - "version": "18.1.3", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", - "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", - "dev": true, - "peer": true, - "dependencies": { - "camelcase": "^5.0.0", - "decamelize": "^1.2.0" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/micromatch": { "version": "4.0.5", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "dev": true, "dependencies": { "braces": "^3.0.2", "picomatch": "^2.3.1" @@ -12074,6 +6332,7 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, "dependencies": { "brace-expansion": "^1.1.7" }, @@ -12085,37 +6344,11 @@ "version": "1.2.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, "funding": { "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/mixin-deep": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.2.tgz", - "integrity": "sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==", - "dev": true, - "peer": true, - "dependencies": { - "for-in": "^1.0.2", - "is-extendable": "^1.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/mkdirp": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", - "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", - "dev": true, - "peer": true, - "dependencies": { - "minimist": "^1.2.6" - }, - "bin": { - "mkdirp": "bin/cmd.js" - } - }, "node_modules/mocha": { "version": "10.2.0", "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.2.0.tgz", @@ -12157,14 +6390,14 @@ } }, "node_modules/mocha-junit-reporter": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/mocha-junit-reporter/-/mocha-junit-reporter-2.2.0.tgz", - "integrity": "sha512-W83Ddf94nfLiTBl24aS8IVyFvO8aRDLlCvb+cKb/VEaN5dEbcqu3CXiTe8MQK2DvzS7oKE1RsFTxzN302GGbDQ==", + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/mocha-junit-reporter/-/mocha-junit-reporter-2.2.1.tgz", + "integrity": "sha512-iDn2tlKHn8Vh8o4nCzcUVW4q7iXp7cC4EB78N0cDHIobLymyHNwe0XG8HEHHjc3hJlXm0Vy6zcrxaIhnI2fWmw==", "dev": true, "dependencies": { "debug": "^4.3.4", "md5": "^2.3.0", - "mkdirp": "~1.0.4", + "mkdirp": "^3.0.0", "strip-ansi": "^6.0.1", "xml": "^1.0.1" }, @@ -12173,15 +6406,18 @@ } }, "node_modules/mocha-junit-reporter/node_modules/mkdirp": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", - "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz", + "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==", "dev": true, "bin": { - "mkdirp": "bin/cmd.js" + "mkdirp": "dist/cjs/src/bin.js" }, "engines": { "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/mocha-junit-reporter/node_modules/strip-ansi": { @@ -12620,41 +6856,12 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, - "node_modules/nanomatch": { - "version": "1.2.13", - "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz", - "integrity": "sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==", - "dev": true, - "peer": true, - "dependencies": { - "arr-diff": "^4.0.0", - "array-unique": "^0.3.2", - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "fragment-cache": "^0.2.1", - "is-windows": "^1.0.2", - "kind-of": "^6.0.2", - "object.pick": "^1.3.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "dev": true }, - "node_modules/natural-compare-lite": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz", - "integrity": "sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==", - "dev": true - }, "node_modules/negotiator": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", @@ -12671,93 +6878,12 @@ "dev": true, "peer": true }, - "node_modules/nice-try": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", - "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", - "dev": true, - "peer": true - }, - "node_modules/nocache": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/nocache/-/nocache-3.0.4.tgz", - "integrity": "sha512-WDD0bdg9mbq6F4mRxEYcPWwfA1vxd0mrvKOyxI7Xj/atfRHVeutzuWByG//jfm4uPzp0y4Kj051EORCBSQMycw==", - "dev": true, - "peer": true, - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/node-dir": { - "version": "0.1.17", - "resolved": "https://registry.npmjs.org/node-dir/-/node-dir-0.1.17.tgz", - "integrity": "sha512-tmPX422rYgofd4epzrNoOXiE8XFZYOcCq1vD7MAXCDO+O+zndlA2ztdKKMa+EeuBG5tHETpr4ml4RGgpqDCCAg==", - "dev": true, - "peer": true, - "dependencies": { - "minimatch": "^3.0.2" - }, - "engines": { - "node": ">= 0.10.5" - } - }, "node_modules/node-ensure": { "version": "0.0.0", "resolved": "https://registry.npmjs.org/node-ensure/-/node-ensure-0.0.0.tgz", "integrity": "sha1-7K52QVDemYYexcgQ/V0Jaxg5Mqc=", "dev": true }, - "node_modules/node-fetch": { - "version": "2.6.7", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", - "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", - "dev": true, - "peer": true, - "dependencies": { - "whatwg-url": "^5.0.0" - }, - "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } - } - }, - "node_modules/node-int64": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", - "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", - "dev": true, - "peer": true - }, - "node_modules/node-localstorage": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/node-localstorage/-/node-localstorage-1.3.1.tgz", - "integrity": "sha512-NMWCSWWc6JbHT5PyWlNT2i8r7PgGYXVntmKawY83k/M0UJScZ5jirb61TLnqKwd815DfBQu+lR3sRw08SPzIaQ==", - "dev": true, - "dependencies": { - "write-file-atomic": "^1.1.4" - }, - "engines": { - "node": ">=0.12" - } - }, - "node_modules/node-localstorage/node_modules/write-file-atomic": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-1.3.4.tgz", - "integrity": "sha512-SdrHoC/yVBPpV0Xq/mUZQIpW2sWXAShb/V4pomcJXh92RuaO+f3UTWItiR3Px+pLnV2PvC2/bfn5cwr5X6Vfxw==", - "dev": true, - "dependencies": { - "graceful-fs": "^4.1.11", - "imurmurhash": "^0.1.4", - "slide": "^1.1.5" - } - }, "node_modules/node-releases": { "version": "2.0.10", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.10.tgz", @@ -12765,20 +6891,6 @@ "dev": true, "peer": true }, - "node_modules/node-stream-zip": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/node-stream-zip/-/node-stream-zip-1.15.0.tgz", - "integrity": "sha512-LN4fydt9TqhZhThkZIVQnF9cwjU3qmUH9h78Mx/K7d3VvfRqqwthLwJEUOEL0QPZ0XQmNN7be5Ggit5+4dq3Bw==", - "dev": true, - "peer": true, - "engines": { - "node": ">=0.12.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/antelle" - } - }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -12809,20 +6921,6 @@ "node": ">=8" } }, - "node_modules/nullthrows": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/nullthrows/-/nullthrows-1.1.1.tgz", - "integrity": "sha512-2vPPEi+Z7WqML2jZYddDIfy5Dqb0r2fze2zTxNNknZaFpVHU3mFB3R+DWeJWGVx0ecvttSGlJTI+WG+8Z4cDWw==", - "dev": true, - "peer": true - }, - "node_modules/ob1": { - "version": "0.70.3", - "resolved": "https://registry.npmjs.org/ob1/-/ob1-0.70.3.tgz", - "integrity": "sha512-Vy9GGhuXgDRY01QA6kdhToPd8AkLdLpX9GjH5kpqluVqTu70mgOm7tpGoJDZGaNbr9nJlJgnipqHJQRPORixIQ==", - "dev": true, - "peer": true - }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -12832,98 +6930,6 @@ "node": ">=0.10.0" } }, - "node_modules/object-copy": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz", - "integrity": "sha512-79LYn6VAb63zgtmAteVOWo9Vdj71ZVBy3Pbse+VqxDpEP83XuujMrGqHIwAXJ5I/aM0zU7dIyIAhifVTPrNItQ==", - "dev": true, - "peer": true, - "dependencies": { - "copy-descriptor": "^0.1.0", - "define-property": "^0.2.5", - "kind-of": "^3.0.3" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-copy/node_modules/define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA==", - "dev": true, - "peer": true, - "dependencies": { - "is-descriptor": "^0.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-copy/node_modules/is-accessor-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", - "integrity": "sha512-e1BM1qnDbMRG3ll2U9dSK0UMHuWOs3pY3AtcFsmvwPtKL3MML/Q86i+GilLfvqEs4GW+ExB91tQ3Ig9noDIZ+A==", - "dev": true, - "peer": true, - "dependencies": { - "kind-of": "^3.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-copy/node_modules/is-data-descriptor": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", - "integrity": "sha512-+w9D5ulSoBNlmw9OHn3U2v51SyoCd0he+bB3xMl62oijhrspxowjU+AIcDY0N3iEJbUEkB15IlMASQsxYigvXg==", - "dev": true, - "peer": true, - "dependencies": { - "kind-of": "^3.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-copy/node_modules/is-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", - "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", - "dev": true, - "peer": true, - "dependencies": { - "is-accessor-descriptor": "^0.1.6", - "is-data-descriptor": "^0.1.4", - "kind-of": "^5.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-copy/node_modules/is-descriptor/node_modules/kind-of": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", - "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", - "dev": true, - "peer": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-copy/node_modules/kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", - "dev": true, - "peer": true, - "dependencies": { - "is-buffer": "^1.1.5" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/object-inspect": { "version": "1.12.3", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz", @@ -12958,19 +6964,6 @@ "node": ">= 0.4" } }, - "node_modules/object-visit": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz", - "integrity": "sha512-GBaMwwAVK9qbQN3Scdo0OyvgPW7l3lnaVMj84uTOZlswkX0KpF6fyDBJhtTthf7pymztoN36/KEr1DyhF96zEA==", - "dev": true, - "peer": true, - "dependencies": { - "isobject": "^3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/object.assign": { "version": "4.1.4", "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.4.tgz", @@ -13020,6 +7013,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/object.groupby": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.1.tgz", + "integrity": "sha512-HqaQtqLnp/8Bn4GL16cj+CUYbnpe1bh0TtEaWvybszDG4tgxCJuRpV8VGuvNaI1fAnI4lUJzDG55MXcOH4JZcQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "get-intrinsic": "^1.2.1" + } + }, "node_modules/object.hasown": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/object.hasown/-/object.hasown-1.1.2.tgz", @@ -13033,19 +7038,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/object.pick": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz", - "integrity": "sha512-tqa/UMy/CCoYmj+H5qc07qvSL9dqcs/WZENZ1JbtWBlATP+iVOe778gE6MSijnyCnORzDuX6hU+LA4SZ09YjFQ==", - "dev": true, - "peer": true, - "dependencies": { - "isobject": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/object.values": { "version": "1.1.6", "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.6.tgz", @@ -13063,33 +7055,11 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/on-finished": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", - "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==", - "dev": true, - "peer": true, - "dependencies": { - "ee-first": "1.1.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/on-headers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", - "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", - "dev": true, - "peer": true, - "engines": { - "node": ">= 0.8" - } - }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dev": true, "dependencies": { "wrappy": "1" } @@ -13109,19 +7079,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/open": { - "version": "6.4.0", - "resolved": "https://registry.npmjs.org/open/-/open-6.4.0.tgz", - "integrity": "sha512-IFenVPgF70fSm1keSd2iDBIDIBZkroLeuffXq+wKTzTJlBpesFWojV9lb8mzOfaAzM1sr7HQHuO0vtV0zYekGg==", - "dev": true, - "peer": true, - "dependencies": { - "is-wsl": "^1.1.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/opener": { "version": "1.5.2", "resolved": "https://registry.npmjs.org/opener/-/opener-1.5.2.tgz", @@ -13132,83 +7089,28 @@ } }, "node_modules/optionator": { - "version": "0.9.1", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", - "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==", + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", + "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==", "dev": true, "dependencies": { + "@aashutoshrathi/word-wrap": "^1.2.3", "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.3" + "type-check": "^0.4.0" }, "engines": { "node": ">= 0.8.0" } }, - "node_modules/ora": { - "version": "5.4.1", - "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", - "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", - "dev": true, - "peer": true, - "dependencies": { - "bl": "^4.1.0", - "chalk": "^4.1.0", - "cli-cursor": "^3.1.0", - "cli-spinners": "^2.5.0", - "is-interactive": "^1.0.0", - "is-unicode-supported": "^0.1.0", - "log-symbols": "^4.1.0", - "strip-ansi": "^6.0.0", - "wcwidth": "^1.0.1" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/ora/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "peer": true, - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/os-tmpdir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", - "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/ospath": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/ospath/-/ospath-1.2.2.tgz", "integrity": "sha1-EnZjl3Sj+O8lcvf+QoDg6kVQwHs=", "dev": true }, - "node_modules/p-finally": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", - "integrity": "sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==", - "dev": true, - "peer": true, - "engines": { - "node": ">=4" - } - }, "node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -13290,20 +7192,6 @@ "node": ">=6" } }, - "node_modules/parse-json": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", - "integrity": "sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw==", - "dev": true, - "peer": true, - "dependencies": { - "error-ex": "^1.3.1", - "json-parse-better-errors": "^1.0.1" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -13313,97 +7201,6 @@ "node": ">= 0.8" } }, - "node_modules/pascalcase": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz", - "integrity": "sha512-XHXfu/yOQRy9vYOtUDVMN60OEJjW013GoObG1o+xwQTpB9eYJX/BjXMsdW13ZDPruFhYYn0AG22w0xgQMwl3Nw==", - "dev": true, - "peer": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/patch-package": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/patch-package/-/patch-package-7.0.0.tgz", - "integrity": "sha512-eYunHbnnB2ghjTNc5iL1Uo7TsGMuXk0vibX3RFcE/CdVdXzmdbMsG/4K4IgoSuIkLTI5oHrMQk4+NkFqSed0BQ==", - "dependencies": { - "@yarnpkg/lockfile": "^1.1.0", - "chalk": "^4.1.2", - "ci-info": "^3.7.0", - "cross-spawn": "^7.0.3", - "find-yarn-workspace-root": "^2.0.0", - "fs-extra": "^9.0.0", - "klaw-sync": "^6.0.0", - "minimist": "^1.2.6", - "open": "^7.4.2", - "rimraf": "^2.6.3", - "semver": "^5.6.0", - "slash": "^2.0.0", - "tmp": "^0.0.33", - "yaml": "^2.2.2" - }, - "bin": { - "patch-package": "index.js" - }, - "engines": { - "node": ">=14", - "npm": ">5" - } - }, - "node_modules/patch-package/node_modules/is-wsl": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", - "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", - "dependencies": { - "is-docker": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/patch-package/node_modules/open": { - "version": "7.4.2", - "resolved": "https://registry.npmjs.org/open/-/open-7.4.2.tgz", - "integrity": "sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==", - "dependencies": { - "is-docker": "^2.0.0", - "is-wsl": "^2.1.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/patch-package/node_modules/semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "bin": { - "semver": "bin/semver" - } - }, - "node_modules/patch-package/node_modules/slash": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz", - "integrity": "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==", - "engines": { - "node": ">=6" - } - }, - "node_modules/patch-package/node_modules/tmp": { - "version": "0.0.33", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", - "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", - "dependencies": { - "os-tmpdir": "~1.0.2" - }, - "engines": { - "node": ">=0.6.0" - } - }, "node_modules/path": { "version": "0.12.7", "resolved": "https://registry.npmjs.org/path/-/path-0.12.7.tgz", @@ -13427,6 +7224,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "dev": true, "engines": { "node": ">=0.10.0" } @@ -13435,6 +7233,7 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, "engines": { "node": ">=8" } @@ -13504,15 +7303,15 @@ "dev": true }, "node_modules/pg": { - "version": "8.10.0", - "resolved": "https://registry.npmjs.org/pg/-/pg-8.10.0.tgz", - "integrity": "sha512-ke7o7qSTMb47iwzOSaZMfeR7xToFdkE71ifIipOAAaLIM0DYzfOAXlgFFmYUIE2BcJtvnVlGCID84ZzCegE8CQ==", + "version": "8.11.3", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.11.3.tgz", + "integrity": "sha512-+9iuvG8QfaaUrrph+kpF24cXkH1YOOUeArRNYIxq1viYHZagBxrTno7cecY1Fa44tJeZvaoG+Djpkc3JwehN5g==", "dev": true, "dependencies": { "buffer-writer": "2.0.0", "packet-reader": "1.0.0", - "pg-connection-string": "^2.5.0", - "pg-pool": "^3.6.0", + "pg-connection-string": "^2.6.2", + "pg-pool": "^3.6.1", "pg-protocol": "^1.6.0", "pg-types": "^2.1.0", "pgpass": "1.x" @@ -13520,6 +7319,9 @@ "engines": { "node": ">= 8.0.0" }, + "optionalDependencies": { + "pg-cloudflare": "^1.1.1" + }, "peerDependencies": { "pg-native": ">=3.0.1" }, @@ -13529,10 +7331,17 @@ } } }, + "node_modules/pg-cloudflare": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.1.1.tgz", + "integrity": "sha512-xWPagP/4B6BgFO+EKz3JONXv3YDgvkbVrGw2mTo3D6tVDQRh1e7cqVGvyR3BE+eQgAvx1XhW/iEASj4/jCWl3Q==", + "dev": true, + "optional": true + }, "node_modules/pg-connection-string": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.5.0.tgz", - "integrity": "sha512-r5o/V/ORTA6TmUnyWZR9nCj1klXCO2CEKNRlVuJptZe85QuhFayC7WeMic7ndayT5IRIR0S0xFxFi2ousartlQ==", + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.6.1.tgz", + "integrity": "sha512-w6ZzNu6oMmIzEAYVw+RLK0+nqHPt8K3ZnknKi+g48Ak2pr3dtljJW3o+D/n2zzCG07Zoe9VOX3aiKpj+BN0pjg==", "dev": true }, "node_modules/pg-int8": { @@ -13545,9 +7354,9 @@ } }, "node_modules/pg-pool": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.6.0.tgz", - "integrity": "sha512-clFRf2ksqd+F497kWFyM21tMjeikn60oGDmqMT8UBrynEwVEX/5R5xd2sdvdo1cZCFlguORNpVuqxIj+aK4cfQ==", + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.6.1.tgz", + "integrity": "sha512-jizsIzhkIitxCGfPRzJn1ZdcosIt3pz9Sh3V01fm1vZnbnCMgmGl5wvGGdNN2EL9Rmb0EcFoCkixH4Pu+sP9Og==", "dev": true, "peerDependencies": { "pg": ">=8.0" @@ -13575,6 +7384,12 @@ "node": ">=4" } }, + "node_modules/pg/node_modules/pg-connection-string": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.6.2.tgz", + "integrity": "sha512-ch6OwaeaPYcova4kKZ15sbJ2hKb/VP48ZD2gE7i1J+L4MspCtBMAx8nMgz7bksc7IojCIIWuEhHibSMFH8m8oA==", + "dev": true + }, "node_modules/pgpass": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", @@ -13595,6 +7410,7 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, "engines": { "node": ">=8.6" }, @@ -13611,125 +7427,12 @@ "node": ">=0.10.0" } }, - "node_modules/pirates": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.5.tgz", - "integrity": "sha512-8V9+HQPupnaXMA23c5hvl69zXvTwTzyAYasnkb0Tts4XvO4CliqONMOnvlq26rkhLC3nWDFBJf73LU1e1VZLaQ==", - "dev": true, - "peer": true, - "engines": { - "node": ">= 6" - } - }, - "node_modules/pkg-dir": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-3.0.0.tgz", - "integrity": "sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw==", - "dev": true, - "peer": true, - "dependencies": { - "find-up": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/pkg-dir/node_modules/find-up": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", - "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", - "dev": true, - "peer": true, - "dependencies": { - "locate-path": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/pkg-dir/node_modules/locate-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", - "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", - "dev": true, - "peer": true, - "dependencies": { - "p-locate": "^3.0.0", - "path-exists": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/pkg-dir/node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dev": true, - "peer": true, - "dependencies": { - "p-try": "^2.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/pkg-dir/node_modules/p-locate": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", - "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", - "dev": true, - "peer": true, - "dependencies": { - "p-limit": "^2.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/pkg-dir/node_modules/path-exists": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", - "integrity": "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==", - "dev": true, - "peer": true, - "engines": { - "node": ">=4" - } - }, "node_modules/platform": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/platform/-/platform-1.3.3.tgz", "integrity": "sha1-ZGx3ARiZhwtqCQPnXpl+jlHadGE=", "dev": true }, - "node_modules/plist": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/plist/-/plist-3.0.6.tgz", - "integrity": "sha512-WiIVYyrp8TD4w8yCvyeIr+lkmrGRd5u0VbRnU+tP/aRLxP/YadJUYOMZJ/6hIa3oUyVCsycXvtNRgd5XBJIbiA==", - "dev": true, - "peer": true, - "dependencies": { - "base64-js": "^1.5.1", - "xmlbuilder": "^15.1.1" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/plist/node_modules/xmlbuilder": { - "version": "15.1.1", - "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.1.1.tgz", - "integrity": "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==", - "dev": true, - "peer": true, - "engines": { - "node": ">=8.0" - } - }, "node_modules/popsicle": { "version": "12.1.0", "resolved": "https://registry.npmjs.org/popsicle/-/popsicle-12.1.0.tgz", @@ -13821,16 +7524,6 @@ "servie": "^4.0.0" } }, - "node_modules/posix-character-classes": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz", - "integrity": "sha512-xTgYBc3fuo7Yt7JbiuFxSYGToMoz8fLoE6TC9Wx1P/u+LfeThMOAqmuyECnlBaaJb+u1m9hHiXUEtwW4OzfUJg==", - "dev": true, - "peer": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/postgres-array": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", @@ -13891,49 +7584,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/pretty-format": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-26.6.2.tgz", - "integrity": "sha512-7AeGuCYNGmycyQbCqd/3PWH4eOoX/OiCa0uphp57NVTeAGdJGaAliecxwBDHYQCIvrW7aDBZCYeNTP/WX69mkg==", - "dev": true, - "peer": true, - "dependencies": { - "@jest/types": "^26.6.2", - "ansi-regex": "^5.0.0", - "ansi-styles": "^4.0.0", - "react-is": "^17.0.1" - }, - "engines": { - "node": ">= 10" - } - }, - "node_modules/pretty-format/node_modules/@jest/types": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-26.6.2.tgz", - "integrity": "sha512-fC6QCp7Sc5sX6g8Tvbmj4XUTbyrik0akgRy03yjXbQaBWWNWGE7SGtJk98m0N8nzegD/7SggrUlivxo5ax4KWQ==", - "dev": true, - "peer": true, - "dependencies": { - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^15.0.0", - "chalk": "^4.0.0" - }, - "engines": { - "node": ">= 10.14.2" - } - }, - "node_modules/pretty-format/node_modules/@types/yargs": { - "version": "15.0.14", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.14.tgz", - "integrity": "sha512-yEJzHoxf6SyQGhBhIYGXQDSCkJjB6HohDShto7m8vaKg9Yp0Yn8+71J9eakh2bnPg6BfsH9PRMhiRTZnd4eXGQ==", - "dev": true, - "peer": true, - "dependencies": { - "@types/yargs-parser": "*" - } - }, "node_modules/process": { "version": "0.11.10", "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", @@ -13949,30 +7599,6 @@ "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", "dev": true }, - "node_modules/promise": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/promise/-/promise-8.1.0.tgz", - "integrity": "sha512-W04AqnILOL/sPRXziNicCjSNRruLAuIHEOVBazepu0545DDNGYHz7ar9ZgZ1fMU8/MA4mVxp5rkBWRi6OXIy3Q==", - "dev": true, - "peer": true, - "dependencies": { - "asap": "~2.0.6" - } - }, - "node_modules/prompts": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", - "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", - "dev": true, - "peer": true, - "dependencies": { - "kleur": "^3.0.3", - "sisteransi": "^1.0.5" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/prop-types": { "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", @@ -14068,6 +7694,12 @@ "node": ">=0.4.x" } }, + "node_modules/querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", + "dev": true + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -14121,153 +7753,12 @@ "node": ">= 0.8" } }, - "node_modules/react": { - "version": "18.0.0", - "resolved": "https://registry.npmjs.org/react/-/react-18.0.0.tgz", - "integrity": "sha512-x+VL6wbT4JRVPm7EGxXhZ8w8LTROaxPXOqhlGyVSrv0sB1jkyFGgXxJ8LVoPRLvPR6/CIZGFmfzqUa2NYeMr2A==", - "dev": true, - "peer": true, - "dependencies": { - "loose-envify": "^1.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/react-devtools-core": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/react-devtools-core/-/react-devtools-core-4.24.0.tgz", - "integrity": "sha512-Rw7FzYOOzcfyUPaAm9P3g0tFdGqGq2LLiAI+wjYcp6CsF3DeeMrRS3HZAho4s273C29G/DJhx0e8BpRE/QZNGg==", - "dev": true, - "peer": true, - "dependencies": { - "shell-quote": "^1.6.1", - "ws": "^7" - } - }, - "node_modules/react-devtools-core/node_modules/ws": { - "version": "7.5.8", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.8.tgz", - "integrity": "sha512-ri1Id1WinAX5Jqn9HejiGb8crfRio0Qgu8+MtL36rlTA6RLsMdWt1Az/19A2Qij6uSHUMphEFaTKa4WG+UNHNw==", - "dev": true, - "peer": true, - "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 - } - } - }, "node_modules/react-is": { "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", "dev": true }, - "node_modules/react-native": { - "version": "0.69.1", - "resolved": "https://registry.npmjs.org/react-native/-/react-native-0.69.1.tgz", - "integrity": "sha512-585NmzSuYUfday8YsfqgreFAZbXRI/kxKsiuaShwGHgkwdtmE5qA7WfGItgxZBOZD6g/Hm1YBUqSwIm07tPa6A==", - "dev": true, - "peer": true, - "dependencies": { - "@jest/create-cache-key-function": "^27.0.1", - "@react-native-community/cli": "^8.0.0", - "@react-native-community/cli-platform-android": "^8.0.0", - "@react-native-community/cli-platform-ios": "^8.0.0", - "@react-native/assets": "1.0.0", - "@react-native/normalize-color": "2.0.0", - "@react-native/polyfills": "2.0.0", - "abort-controller": "^3.0.0", - "anser": "^1.4.9", - "base64-js": "^1.1.2", - "event-target-shim": "^5.0.1", - "hermes-engine": "~0.11.0", - "invariant": "^2.2.4", - "jsc-android": "^250230.2.1", - "memoize-one": "^5.0.0", - "metro-react-native-babel-transformer": "0.70.3", - "metro-runtime": "0.70.3", - "metro-source-map": "0.70.3", - "mkdirp": "^0.5.1", - "nullthrows": "^1.1.1", - "pretty-format": "^26.5.2", - "promise": "^8.0.3", - "react-devtools-core": "4.24.0", - "react-native-codegen": "^0.69.1", - "react-native-gradle-plugin": "^0.0.7", - "react-refresh": "^0.4.0", - "react-shallow-renderer": "16.14.1", - "regenerator-runtime": "^0.13.2", - "scheduler": "^0.21.0", - "stacktrace-parser": "^0.1.3", - "use-sync-external-store": "^1.0.0", - "whatwg-fetch": "^3.0.0", - "ws": "^6.1.4" - }, - "bin": { - "react-native": "cli.js" - }, - "engines": { - "node": ">=14" - }, - "peerDependencies": { - "react": "18.0.0" - } - }, - "node_modules/react-native-codegen": { - "version": "0.69.1", - "resolved": "https://registry.npmjs.org/react-native-codegen/-/react-native-codegen-0.69.1.tgz", - "integrity": "sha512-TOZEqBarczcyYN3iZE3VpKkooOevaAzBq9n7lU0h9mQUvtRhLVyolc+a5K6cWI0e4v4K69I0MqzjPcPeFKo32Q==", - "dev": true, - "peer": true, - "dependencies": { - "@babel/parser": "^7.14.0", - "flow-parser": "^0.121.0", - "jscodeshift": "^0.13.1", - "nullthrows": "^1.1.1" - } - }, - "node_modules/react-native-gradle-plugin": { - "version": "0.0.7", - "resolved": "https://registry.npmjs.org/react-native-gradle-plugin/-/react-native-gradle-plugin-0.0.7.tgz", - "integrity": "sha512-+4JpbIx42zGTONhBTIXSyfyHICHC29VTvhkkoUOJAh/XHPEixpuBduYgf6Y4y9wsN1ARlQhBBoptTvXvAFQf5g==", - "dev": true, - "peer": true - }, - "node_modules/react-refresh": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.4.3.tgz", - "integrity": "sha512-Hwln1VNuGl/6bVwnd0Xdn1e84gT/8T9aYNL+HAKDArLCS7LWjwr7StE30IEYbIkx0Vi3vs+coQxe+SQDbGbbpA==", - "dev": true, - "peer": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/react-shallow-renderer": { - "version": "16.14.1", - "resolved": "https://registry.npmjs.org/react-shallow-renderer/-/react-shallow-renderer-16.14.1.tgz", - "integrity": "sha512-rkIMcQi01/+kxiTE9D3fdS959U1g7gs+/rborw++42m1O9FAQiNI/UNRZExVUoAOprn4umcXf+pFRou8i4zuBg==", - "dev": true, - "peer": true, - "dependencies": { - "object-assign": "^4.1.1", - "react-is": "^16.12.0 || ^17.0.0" - }, - "peerDependencies": { - "react": "^16.0.0 || ^17.0.0" - } - }, "node_modules/readable-stream": { "version": "2.3.7", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", @@ -14301,39 +7792,6 @@ "node": ">=8.10.0" } }, - "node_modules/readline": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/readline/-/readline-1.3.0.tgz", - "integrity": "sha512-k2d6ACCkiNYz222Fs/iNze30rRJ1iIicW7JuX/7/cozvih6YCkFZH+J6mAFDVgv0dRBaAyr4jDqC95R2y4IADg==", - "dev": true, - "peer": true - }, - "node_modules/recast": { - "version": "0.20.5", - "resolved": "https://registry.npmjs.org/recast/-/recast-0.20.5.tgz", - "integrity": "sha512-E5qICoPoNL4yU0H0NoBDntNB0Q5oMSNh9usFctYniLBluTthi3RsQVBXIJNbApOlvSwW/RGxIuokPcAc59J5fQ==", - "dev": true, - "peer": true, - "dependencies": { - "ast-types": "0.14.2", - "esprima": "~4.0.0", - "source-map": "~0.6.1", - "tslib": "^2.0.1" - }, - "engines": { - "node": ">= 4" - } - }, - "node_modules/recast/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "peer": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/rechoir": { "version": "0.6.2", "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz", @@ -14358,155 +7816,18 @@ "node": ">=6.0.0" } }, - "node_modules/redux": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/redux/-/redux-4.0.5.tgz", - "integrity": "sha512-VSz1uMAH24DM6MF72vcojpYPtrTUu3ByVWfPL1nPfVRb5mZVTve5GnNCUV53QM/BZ66xfWrm0CTWoM+Xlz8V1w==", - "dev": true, - "dependencies": { - "loose-envify": "^1.4.0", - "symbol-observable": "^1.2.0" - } - }, - "node_modules/redux-action-buffer": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/redux-action-buffer/-/redux-action-buffer-1.2.0.tgz", - "integrity": "sha512-SvXSJQrn1Nsmza+xVMlvqZf0eiHIPV3I796jVC2DCC8X8+JXpAaSFuNxlxV0Cn+TAilat7KZ3srTwG5+HHL8tw==", - "dev": true - }, - "node_modules/redux-devtools-core": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/redux-devtools-core/-/redux-devtools-core-0.2.1.tgz", - "integrity": "sha512-RAGOxtUFdr/1USAvxrWd+Gq/Euzgw7quCZlO5TgFpDfG7rB5tMhZUrNyBjpzgzL2yMk0eHnPYIGm7NkIfRzHxQ==", - "deprecated": "Package moved to @redux-devtools/app.", - "dev": true, - "dependencies": { - "get-params": "^0.1.2", - "jsan": "^3.1.13", - "lodash": "^4.17.11", - "nanoid": "^2.0.0", - "remotedev-serialize": "^0.1.8" - } - }, - "node_modules/redux-devtools-core/node_modules/nanoid": { - "version": "2.1.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-2.1.11.tgz", - "integrity": "sha512-s/snB+WGm6uwi0WjsZdaVcuf3KJXlfGl2LcxgwkEwJF0D/BWzVWAZW/XY4bFaiR7s0Jk3FPvlnepg1H1b1UwlA==", - "dev": true - }, - "node_modules/redux-devtools-instrument": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/redux-devtools-instrument/-/redux-devtools-instrument-1.10.0.tgz", - "integrity": "sha512-X8JRBCzX2ADSMp+iiV7YQ8uoTNyEm0VPFPd4T854coz6lvRiBrFSqAr9YAS2n8Kzxx8CJQotR0QF9wsMM+3DvA==", - "deprecated": "Package moved to @redux-devtools/instrument.", - "dev": true, - "dependencies": { - "lodash": "^4.17.19", - "symbol-observable": "^1.2.0" - }, - "peerDependencies": { - "redux": "^3.4.0 || ^4.0.0" - } - }, - "node_modules/redux-offline": { - "version": "1.1.5", - "resolved": "git+ssh://git@github.com/enahum/redux-offline.git#885024de96b6ec73650c340c8928066585c413df", - "integrity": "sha512-srmJ1vWm8ZQTYflZCf7oUs3WBX83GyCIzsFUpwxUg2wcDHngSHjjShRTCgmkciPkVmM4aJ33i9baYS9jRC+zLA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@react-native-community/netinfo": "^4.1.3", - "redux-persist": "^4.5.0" - }, - "peerDependencies": { - "redux": ">=3" - } - }, - "node_modules/redux-persist": { - "version": "4.9.1", - "resolved": "https://registry.npmjs.org/redux-persist/-/redux-persist-4.9.1.tgz", - "integrity": "sha512-XoOmfPyo9GEU/WLH9FgB47dNIN9l5ArjHes4o7vUWx9nxZoPxnVodhuHdyc4Ot+fMkdj3L2LTqSHhwrkr0QFUg==", - "dev": true, - "dependencies": { - "json-stringify-safe": "^5.0.1", - "lodash": "^4.17.4", - "lodash-es": "^4.17.4" - } - }, - "node_modules/redux-persist-node-storage": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/redux-persist-node-storage/-/redux-persist-node-storage-2.0.0.tgz", - "integrity": "sha512-nytPz/iNTrAO4o8A17UaipPl8tVcrnm84r6v0tgHJ+q0ysEzyS/rBlPGXrKNIPplXi/W4riUTQlCIhajodIfJg==", - "dev": true, - "dependencies": { - "node-localstorage": "^1.3.0" - } - }, - "node_modules/redux-thunk": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-2.3.0.tgz", - "integrity": "sha512-km6dclyFnmcvxhAcrQV2AkZmPQjzPDjgVlQtR0EQjxZPyJ0BnMf3in1ryuR8A2qU0HldVRfxYXbFSKlI3N7Slw==", - "dev": true - }, - "node_modules/regenerate": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", - "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==", - "dev": true, - "peer": true - }, - "node_modules/regenerate-unicode-properties": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.1.0.tgz", - "integrity": "sha512-d1VudCLoIGitcU/hEg2QqvyGZQmdC0Lf8BqdOMXGFSvJP4bNV1+XqbPQeHHLD51Jh4QJJ225dlIFvY4Ly6MXmQ==", - "dev": true, - "peer": true, - "dependencies": { - "regenerate": "^1.4.2" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/regenerator-runtime": { - "version": "0.13.11", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", - "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==", - "dev": true - }, - "node_modules/regenerator-transform": { - "version": "0.15.0", - "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.15.0.tgz", - "integrity": "sha512-LsrGtPmbYg19bcPHwdtmXwbW+TqNvtY4riE3P83foeHRroMbH6/2ddFBfab3t7kbzc7v7p4wbkIecHImqt0QNg==", - "dev": true, - "peer": true, - "dependencies": { - "@babel/runtime": "^7.8.4" - } - }, - "node_modules/regex-not": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz", - "integrity": "sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==", - "dev": true, - "peer": true, - "dependencies": { - "extend-shallow": "^3.0.2", - "safe-regex": "^1.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/regexp.prototype.flags": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.4.3.tgz", - "integrity": "sha512-fjggEOO3slI6Wvgjwflkc4NFRCTZAu5CnNfBd5qOMYhWdn67nJBBu34/TkD++eeFmd8C9r9jfXJ27+nSiRkSUA==", + "node_modules/reflect.getprototypeof": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.4.tgz", + "integrity": "sha512-ECkTw8TmJwW60lOTR+ZkODISW6RQ8+2CL3COqtiJKLd6MmB45hN51HprHFziKLGkAuTGQhBb91V8cy+KHlaCjw==", "dev": true, "dependencies": { "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "functions-have-names": "^1.2.2" + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "get-intrinsic": "^1.2.1", + "globalthis": "^1.0.3", + "which-builtin-type": "^1.1.3" }, "engines": { "node": ">= 0.4" @@ -14515,89 +7836,27 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/regexpu-core": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-5.3.2.tgz", - "integrity": "sha512-RAM5FlZz+Lhmo7db9L298p2vHP5ZywrVXmVXpmAD9GuL5MPH6t9ROw1iA/wfHkQ76Qe7AaPF0nGuim96/IrQMQ==", + "node_modules/regenerator-runtime": { + "version": "0.13.11", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", + "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==", + "dev": true + }, + "node_modules/regexp.prototype.flags": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.1.tgz", + "integrity": "sha512-sy6TXMN+hnP/wMy+ISxg3krXx7BAtWVO4UouuCN/ziM9UEne0euamVNafDfvC83bRNr95y0V5iijeDQFUNpvrg==", "dev": true, - "peer": true, "dependencies": { - "@babel/regjsgen": "^0.8.0", - "regenerate": "^1.4.2", - "regenerate-unicode-properties": "^10.1.0", - "regjsparser": "^0.9.1", - "unicode-match-property-ecmascript": "^2.0.0", - "unicode-match-property-value-ecmascript": "^2.1.0" + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "set-function-name": "^2.0.0" }, "engines": { - "node": ">=4" - } - }, - "node_modules/regjsparser": { - "version": "0.9.1", - "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.9.1.tgz", - "integrity": "sha512-dQUtn90WanSNl+7mQKcXAgZxvUe7Z0SqXlgzv0za4LwiUhyzBC58yQO3liFoUgu8GiJVInAhJjkj1N0EtQ5nkQ==", - "dev": true, - "peer": true, - "dependencies": { - "jsesc": "~0.5.0" + "node": ">= 0.4" }, - "bin": { - "regjsparser": "bin/parser" - } - }, - "node_modules/regjsparser/node_modules/jsesc": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", - "integrity": "sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==", - "dev": true, - "peer": true, - "bin": { - "jsesc": "bin/jsesc" - } - }, - "node_modules/remote-redux-devtools": { - "version": "0.5.16", - "resolved": "https://registry.npmjs.org/remote-redux-devtools/-/remote-redux-devtools-0.5.16.tgz", - "integrity": "sha512-xZ2D1VRIWzat5nsvcraT6fKEX9Cfi+HbQBCwzNnUAM8Uicm/anOc60XGalcaDPrVmLug7nhDl2nimEa3bL3K9w==", - "dev": true, - "dependencies": { - "jsan": "^3.1.13", - "querystring": "^0.2.0", - "redux-devtools-core": "^0.2.1", - "redux-devtools-instrument": "^1.9.4", - "rn-host-detect": "^1.1.5", - "socketcluster-client": "^14.2.1" - } - }, - "node_modules/remotedev-serialize": { - "version": "0.1.9", - "resolved": "https://registry.npmjs.org/remotedev-serialize/-/remotedev-serialize-0.1.9.tgz", - "integrity": "sha512-5tFdZg9mSaAWTv6xmQ7HtHjKMLSFQFExEZOtJe10PLsv1wb7cy7kYHtBvTYRro27/3fRGEcQBRNKSaixOpb69w==", - "deprecated": "Package moved to @redux-devtools/serialize.", - "dev": true, - "dependencies": { - "jsan": "^3.1.13" - } - }, - "node_modules/repeat-element": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.4.tgz", - "integrity": "sha512-LFiNfRcSu7KK3evMyYOuCzv3L10TW7yC1G2/+StMjK8Y6Vqd2MG7r/Qjw4ghtuCOjFvlnms/iMmLqpvW/ES/WQ==", - "dev": true, - "peer": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/repeat-string": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", - "integrity": "sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==", - "dev": true, - "peer": true, - "engines": { - "node": ">=0.10" + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/request-progress": { @@ -14624,10 +7883,10 @@ "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", "dev": true }, - "node_modules/reselect": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/reselect/-/reselect-4.0.0.tgz", - "integrity": "sha512-qUgANli03jjAyGlnbYVAV5vvnOmJnODyABz51RdBN7M4WaVu8mecZWgyQNkG8Yqe3KRGRt0l4K4B3XVEULC4CA==", + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", "dev": true }, "node_modules/resolve": { @@ -14643,24 +7902,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/resolve-from": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-3.0.0.tgz", - "integrity": "sha512-GnlH6vxLymXJNMBo7XP1fJIzBFbdYt49CuTwmB/6N53t+kMPRMFKz783LlQ4tv28XoQfMWinAJX6WCGf2IlaIw==", - "dev": true, - "peer": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/resolve-url": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", - "integrity": "sha512-ZuF55hVUQaaczgOIwqWzkEcEidmlD/xl44x1UZnhOXcYuFN2S6+rcxpG+C1N3So0wvNI3DmJICUFfu2SxhBmvg==", - "deprecated": "https://github.com/lydell/resolve-url#deprecated", - "dev": true, - "peer": true - }, "node_modules/restore-cursor": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", @@ -14674,16 +7915,6 @@ "node": ">=8" } }, - "node_modules/ret": { - "version": "0.1.15", - "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", - "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==", - "dev": true, - "peer": true, - "engines": { - "node": ">=0.12" - } - }, "node_modules/reusify": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", @@ -14694,30 +7925,6 @@ "node": ">=0.10.0" } }, - "node_modules/rimraf": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", - "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - } - }, - "node_modules/rn-host-detect": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/rn-host-detect/-/rn-host-detect-1.2.0.tgz", - "integrity": "sha512-btNg5kzHcjZZ7t7mvvV/4wNJ9e3MPgrWivkRgWURzXL0JJ0pwWlU4zrbmdlz3HHzHOxhBhHB4D+/dbMFfu4/4A==", - "dev": true - }, - "node_modules/rudder-sdk-js": { - "version": "1.0.14", - "resolved": "https://registry.npmjs.org/rudder-sdk-js/-/rudder-sdk-js-1.0.14.tgz", - "integrity": "sha512-q86qmF6VUXjTUCv1YRFc2V1pt408D641nD2ymKFaLtGZTLjkZCzotQ7cW8c48vPKlSWwekkBlA3kghy/NKGwOw==", - "deprecated": "1.x.x versions of the SDK are deprecated. Please upgrade to the latest (2.x.x) version", - "dev": true - }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -14759,6 +7966,30 @@ "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", "dev": true }, + "node_modules/safe-array-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.0.1.tgz", + "integrity": "sha512-6XbUAseYE2KtOuGueyeobCySj9L4+66Tn6KQMOPQJrAJEowYKW/YR/MGJZl7FdydUdaFu4LYyDZjxf4/Nmo23Q==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.2.1", + "has-symbols": "^1.0.3", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-array-concat/node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true + }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -14779,16 +8010,6 @@ } ] }, - "node_modules/safe-regex": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", - "integrity": "sha512-aJXcif4xnaNUzvUuC5gcb46oTS7zvg4jpMTnuqtrEPlR3vFr4pxtdTwaF1Qs3Enjn9HK+ZlwQui+a7z0SywIzg==", - "dev": true, - "peer": true, - "dependencies": { - "ret": "~0.1.10" - } - }, "node_modules/safe-regex-test": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.0.tgz", @@ -14815,37 +8036,6 @@ "integrity": "sha512-8I2a3LovHTOpm7NV5yOyO8IHqgVsfK4+UuySrXU8YXkSRX7k6hCV9b3HrkKCr3nMpgj+0bmocaJJWpvp1oc7ZA==", "dev": true }, - "node_modules/sc-channel": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/sc-channel/-/sc-channel-1.2.0.tgz", - "integrity": "sha512-M3gdq8PlKg0zWJSisWqAsMmTVxYRTpVRqw4CWAdKBgAfVKumFcTjoCV0hYu7lgUXccCtCD8Wk9VkkE+IXCxmZA==", - "dev": true, - "dependencies": { - "component-emitter": "1.2.1" - } - }, - "node_modules/sc-errors": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/sc-errors/-/sc-errors-2.0.1.tgz", - "integrity": "sha512-JoVhq3Ud+3Ujv2SIG7W0XtjRHsrNgl6iXuHHsh0s+Kdt5NwI6N2EGAZD4iteitdDv68ENBkpjtSvN597/wxPSQ==", - "dev": true - }, - "node_modules/sc-formatter": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/sc-formatter/-/sc-formatter-3.0.3.tgz", - "integrity": "sha512-lYI/lTs1u1c0geKElcj+bmEUfcP/HuKg2iDeTijPSjiTNFzN3Cf8Qh6tVd65oi7Qn+2/oD7LP4s6GC13v/9NiQ==", - "dev": true - }, - "node_modules/scheduler": { - "version": "0.21.0", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.21.0.tgz", - "integrity": "sha512-1r87x5fz9MXqswA2ERLo0EbOAU74DpIUO090gIasYTqlVoJeMcl+Z1Rg7WHz+qtPujhS/hGIt9kxZOYBV3faRQ==", - "dev": true, - "peer": true, - "dependencies": { - "loose-envify": "^1.1.0" - } - }, "node_modules/schema-utils": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.1.tgz", @@ -14952,33 +8142,6 @@ "node": ">= 0.8" } }, - "node_modules/serialize-error": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-6.0.0.tgz", - "integrity": "sha512-3vmBkMZLQO+BR4RPHcyRGdE09XCF6cvxzk2N2qn8Er3F91cy8Qt7VvEbZBOpaL53qsBbe2cFOefU6tRY6WDelA==", - "dev": true, - "dependencies": { - "type-fest": "^0.12.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/serialize-error/node_modules/type-fest": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.12.0.tgz", - "integrity": "sha512-53RyidyjvkGpnWPMF9bQgFtWp+Sl8O2Rp13VavmJgfAP9WWG6q6TkrKU8iyJdnwnfgHI6k2hTlgqH4aSdjoTbg==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/serialize-javascript": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz", @@ -15020,43 +8183,18 @@ "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", "dev": true }, - "node_modules/set-value": { + "node_modules/set-function-name": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz", - "integrity": "sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.1.tgz", + "integrity": "sha512-tMNCiqYVkXIZgc2Hnoy2IvC/f8ezc5koaRFkCjrpWzGpCd3qbZXPzVy9MAZzK1ch/X0jvSkojys3oqJN0qCmdA==", "dev": true, - "peer": true, "dependencies": { - "extend-shallow": "^2.0.1", - "is-extendable": "^0.1.1", - "is-plain-object": "^2.0.3", - "split-string": "^3.0.1" + "define-data-property": "^1.0.1", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.0" }, "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/set-value/node_modules/extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", - "dev": true, - "peer": true, - "dependencies": { - "is-extendable": "^0.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/set-value/node_modules/is-extendable": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", - "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", - "dev": true, - "peer": true, - "engines": { - "node": ">=0.10.0" + "node": ">= 0.4" } }, "node_modules/setprototypeof": { @@ -15065,29 +8203,11 @@ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", "dev": true }, - "node_modules/shallow-clone": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", - "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", - "dev": true, - "peer": true, - "dependencies": { - "kind-of": "^6.0.2" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shallow-equals": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/shallow-equals/-/shallow-equals-1.0.0.tgz", - "integrity": "sha512-xd/FKcdmfmMbyYCca3QTVEJtqUOGuajNzvAX6nt8dXILwjAIEkfHc4hI8/JMGApAmb7VeULO0Q30NTxnbH/15g==", - "dev": true - }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, "dependencies": { "shebang-regex": "^3.0.0" }, @@ -15099,17 +8219,11 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, "engines": { "node": ">=8" } }, - "node_modules/shell-quote": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.7.3.tgz", - "integrity": "sha512-Vpfqwm4EnqGdlsBFNmHhxhElJYrdfcxPThu+ryKS5J8L/fhAwLazFZtq+S+TWZ9ANj2piSQLGj6NQg+lKPmxrw==", - "dev": true, - "peer": true - }, "node_modules/shelljs": { "version": "0.8.5", "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.8.5.tgz", @@ -15156,13 +8270,6 @@ "integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==", "dev": true }, - "node_modules/sisteransi": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", - "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", - "dev": true, - "peer": true - }, "node_modules/slash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", @@ -15186,327 +8293,6 @@ "node": ">=8" } }, - "node_modules/slide": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/slide/-/slide-1.1.6.tgz", - "integrity": "sha512-NwrtjCg+lZoqhFU8fOwl4ay2ei8PaqCBOUV3/ektPY9trO1yQ1oXEfmHAhKArUVUr/hOHvy5f6AdP17dCM0zMw==", - "dev": true, - "engines": { - "node": "*" - } - }, - "node_modules/snapdragon": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz", - "integrity": "sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==", - "dev": true, - "peer": true, - "dependencies": { - "base": "^0.11.1", - "debug": "^2.2.0", - "define-property": "^0.2.5", - "extend-shallow": "^2.0.1", - "map-cache": "^0.2.2", - "source-map": "^0.5.6", - "source-map-resolve": "^0.5.0", - "use": "^3.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/snapdragon-node": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/snapdragon-node/-/snapdragon-node-2.1.1.tgz", - "integrity": "sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==", - "dev": true, - "peer": true, - "dependencies": { - "define-property": "^1.0.0", - "isobject": "^3.0.0", - "snapdragon-util": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/snapdragon-node/node_modules/define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha512-cZTYKFWspt9jZsMscWo8sc/5lbPC9Q0N5nBLgb+Yd915iL3udB1uFgS3B8YCx66UVHq018DAVFoee7x+gxggeA==", - "dev": true, - "peer": true, - "dependencies": { - "is-descriptor": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/snapdragon-util": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/snapdragon-util/-/snapdragon-util-3.0.1.tgz", - "integrity": "sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==", - "dev": true, - "peer": true, - "dependencies": { - "kind-of": "^3.2.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/snapdragon-util/node_modules/kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", - "dev": true, - "peer": true, - "dependencies": { - "is-buffer": "^1.1.5" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/snapdragon/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "peer": true, - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/snapdragon/node_modules/define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA==", - "dev": true, - "peer": true, - "dependencies": { - "is-descriptor": "^0.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/snapdragon/node_modules/extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", - "dev": true, - "peer": true, - "dependencies": { - "is-extendable": "^0.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/snapdragon/node_modules/is-accessor-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", - "integrity": "sha512-e1BM1qnDbMRG3ll2U9dSK0UMHuWOs3pY3AtcFsmvwPtKL3MML/Q86i+GilLfvqEs4GW+ExB91tQ3Ig9noDIZ+A==", - "dev": true, - "peer": true, - "dependencies": { - "kind-of": "^3.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/snapdragon/node_modules/is-accessor-descriptor/node_modules/kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", - "dev": true, - "peer": true, - "dependencies": { - "is-buffer": "^1.1.5" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/snapdragon/node_modules/is-data-descriptor": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", - "integrity": "sha512-+w9D5ulSoBNlmw9OHn3U2v51SyoCd0he+bB3xMl62oijhrspxowjU+AIcDY0N3iEJbUEkB15IlMASQsxYigvXg==", - "dev": true, - "peer": true, - "dependencies": { - "kind-of": "^3.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/snapdragon/node_modules/is-data-descriptor/node_modules/kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", - "dev": true, - "peer": true, - "dependencies": { - "is-buffer": "^1.1.5" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/snapdragon/node_modules/is-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", - "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", - "dev": true, - "peer": true, - "dependencies": { - "is-accessor-descriptor": "^0.1.6", - "is-data-descriptor": "^0.1.4", - "kind-of": "^5.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/snapdragon/node_modules/is-extendable": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", - "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", - "dev": true, - "peer": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/snapdragon/node_modules/kind-of": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", - "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", - "dev": true, - "peer": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/snapdragon/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true, - "peer": true - }, - "node_modules/snapdragon/node_modules/source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", - "dev": true, - "peer": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/socketcluster-client": { - "version": "14.3.2", - "resolved": "https://registry.npmjs.org/socketcluster-client/-/socketcluster-client-14.3.2.tgz", - "integrity": "sha512-xDtgW7Ss0ARlfhx53bJ5GY5THDdEOeJnT+/C9Rmrj/vnZr54xeiQfrCZJbcglwe732nK3V+uZq87IvrRl7Hn4g==", - "dev": true, - "dependencies": { - "buffer": "^5.2.1", - "clone": "2.1.1", - "component-emitter": "1.2.1", - "linked-list": "0.1.0", - "querystring": "0.2.0", - "sc-channel": "^1.2.0", - "sc-errors": "^2.0.1", - "sc-formatter": "^3.0.1", - "uuid": "3.2.1", - "ws": "^7.5.0" - } - }, - "node_modules/socketcluster-client/node_modules/buffer": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", - "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.1.13" - } - }, - "node_modules/socketcluster-client/node_modules/uuid": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.2.1.tgz", - "integrity": "sha512-jZnMwlb9Iku/O3smGWvZhauCf6cvvpKi4BKRiliS3cxnI+Gz9j5MEpTz2UFuXiKPJocb7gnsLHwiS05ige5BEA==", - "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.", - "dev": true, - "bin": { - "uuid": "bin/uuid" - } - }, - "node_modules/socketcluster-client/node_modules/ws": { - "version": "7.5.8", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.8.tgz", - "integrity": "sha512-ri1Id1WinAX5Jqn9HejiGb8crfRio0Qgu8+MtL36rlTA6RLsMdWt1Az/19A2Qij6uSHUMphEFaTKa4WG+UNHNw==", - "dev": true, - "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 - } - } - }, - "node_modules/source-map": { - "version": "0.7.4", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", - "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", - "dev": true, - "peer": true, - "engines": { - "node": ">= 8" - } - }, - "node_modules/source-map-resolve": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.3.tgz", - "integrity": "sha512-Htz+RnsXWk5+P2slx5Jh3Q66vhQj1Cllm0zvnaY98+NFx+Dv2CF/f5O/t8x+KaNdrdIAsruNzoh/KpialbqAnw==", - "deprecated": "See https://github.com/lydell/source-map-resolve#deprecated", - "dev": true, - "peer": true, - "dependencies": { - "atob": "^2.1.2", - "decode-uri-component": "^0.2.0", - "resolve-url": "^0.2.1", - "source-map-url": "^0.4.0", - "urix": "^0.1.0" - } - }, "node_modules/source-map-support": { "version": "0.5.21", "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", @@ -15528,27 +8314,6 @@ "node": ">=0.10.0" } }, - "node_modules/source-map-url": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.1.tgz", - "integrity": "sha512-cPiFOTLUKvJFIg4SKVScy4ilPPW6rFgMgfuZJPNoDuMs3nC1HbMUycBoJw77xFIp6z1UJQJOfx6C9GMH80DiTw==", - "deprecated": "See https://github.com/lydell/source-map-url#deprecated", - "dev": true, - "peer": true - }, - "node_modules/split-string": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", - "integrity": "sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==", - "dev": true, - "peer": true, - "dependencies": { - "extend-shallow": "^3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/split2": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/split2/-/split2-4.1.0.tgz", @@ -15558,13 +8323,6 @@ "node": ">= 10.x" } }, - "node_modules/sprintf-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", - "dev": true, - "peer": true - }, "node_modules/sqlstring": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.1.tgz", @@ -15599,150 +8357,6 @@ "node": ">=0.10.0" } }, - "node_modules/stackframe": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/stackframe/-/stackframe-1.3.4.tgz", - "integrity": "sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==", - "dev": true, - "peer": true - }, - "node_modules/stacktrace-parser": { - "version": "0.1.10", - "resolved": "https://registry.npmjs.org/stacktrace-parser/-/stacktrace-parser-0.1.10.tgz", - "integrity": "sha512-KJP1OCML99+8fhOHxwwzyWrlUuVX5GQ0ZpJTd1DFXhdkrvg1szxfHhawXUZ3g9TkXORQd4/WG68jMlQZ2p8wlg==", - "dev": true, - "peer": true, - "dependencies": { - "type-fest": "^0.7.1" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/stacktrace-parser/node_modules/type-fest": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.7.1.tgz", - "integrity": "sha512-Ne2YiiGN8bmrmJJEuTWTLJR32nh/JdL1+PSicowtNb0WFpn59GK8/lfD61bVtzguz7b3PBt74nxpv/Pw5po5Rg==", - "dev": true, - "peer": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/static-extend": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz", - "integrity": "sha512-72E9+uLc27Mt718pMHt9VMNiAL4LMsmDbBva8mxWUCkT07fSzEGMYUCk0XWY6lp0j6RBAG4cJ3mWuZv2OE3s0g==", - "dev": true, - "peer": true, - "dependencies": { - "define-property": "^0.2.5", - "object-copy": "^0.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/static-extend/node_modules/define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA==", - "dev": true, - "peer": true, - "dependencies": { - "is-descriptor": "^0.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/static-extend/node_modules/is-accessor-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", - "integrity": "sha512-e1BM1qnDbMRG3ll2U9dSK0UMHuWOs3pY3AtcFsmvwPtKL3MML/Q86i+GilLfvqEs4GW+ExB91tQ3Ig9noDIZ+A==", - "dev": true, - "peer": true, - "dependencies": { - "kind-of": "^3.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/static-extend/node_modules/is-accessor-descriptor/node_modules/kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", - "dev": true, - "peer": true, - "dependencies": { - "is-buffer": "^1.1.5" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/static-extend/node_modules/is-data-descriptor": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", - "integrity": "sha512-+w9D5ulSoBNlmw9OHn3U2v51SyoCd0he+bB3xMl62oijhrspxowjU+AIcDY0N3iEJbUEkB15IlMASQsxYigvXg==", - "dev": true, - "peer": true, - "dependencies": { - "kind-of": "^3.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/static-extend/node_modules/is-data-descriptor/node_modules/kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", - "dev": true, - "peer": true, - "dependencies": { - "is-buffer": "^1.1.5" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/static-extend/node_modules/is-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", - "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", - "dev": true, - "peer": true, - "dependencies": { - "is-accessor-descriptor": "^0.1.6", - "is-data-descriptor": "^0.1.4", - "kind-of": "^5.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/static-extend/node_modules/kind-of": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", - "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", - "dev": true, - "peer": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/statuses": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", - "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", - "dev": true, - "peer": true, - "engines": { - "node": ">= 0.6" - } - }, "node_modules/stop-iteration-iterator": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.0.0.tgz", @@ -15816,14 +8430,14 @@ } }, "node_modules/string.prototype.trim": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.7.tgz", - "integrity": "sha512-p6TmeT1T3411M8Cgg9wBTMRtY2q9+PNy9EV1i2lIXUN/btt763oIfxwN3RR8VU6wHX8j/1CFy0L+YuThm6bgOg==", + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.8.tgz", + "integrity": "sha512-lfjY4HcixfQXOfaqCvcBuOIapyaroTXhbkfJN3gcB1OtyupngWK4sEET9Knd0cXd28kTUqu/kHoV4HKSJdnjiQ==", "dev": true, "dependencies": { "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4" + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" }, "engines": { "node": ">= 0.4" @@ -15833,56 +8447,33 @@ } }, "node_modules/string.prototype.trimend": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.6.tgz", - "integrity": "sha512-JySq+4mrPf9EsDBEDYMOb/lM7XQLulwg5R/m1r0PXEFqrV0qHvl58sdTilSXtKOflCsK2E8jxf+GKC0T07RWwQ==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.7.tgz", + "integrity": "sha512-Ni79DqeB72ZFq1uH/L6zJ+DKZTkOtPIHovb3YZHQViE+HDouuU4mBrLOLDn5Dde3RF8qw5qVETEjhu9locMLvA==", "dev": true, "dependencies": { "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4" + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/string.prototype.trimstart": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.6.tgz", - "integrity": "sha512-omqjMDaY92pbn5HOX7f9IccLA+U1tA9GvtU4JrodiXFfYB7jPzzHpRzpglLAjtUV6bB557zwClJezTqnAiYnQA==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.7.tgz", + "integrity": "sha512-NGhtDFu3jCEm7B4Fy0DpLewdJQOZcQ0rGbwQ/+stjnrp2i+rlKeCvos9hOIeCmqwratM47OBxY7uFZzjxHXmrg==", "dev": true, "dependencies": { "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4" + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/strip-ansi": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", - "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", - "dev": true, - "peer": true, - "dependencies": { - "ansi-regex": "^4.1.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/strip-ansi/node_modules/ansi-regex": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz", - "integrity": "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==", - "dev": true, - "peer": true, - "engines": { - "node": ">=6" - } - }, "node_modules/strip-bom": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", @@ -15892,16 +8483,6 @@ "node": ">=4" } }, - "node_modules/strip-eof": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", - "integrity": "sha512-7FCwGGmx8mD5xQd3RPUvnSpUXHM3BWuzjtpD4TXsfcZ9EL4azvVVUscFYwD9nx8Kh+uCBC00XBtAykoMHwTh8Q==", - "dev": true, - "peer": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/strip-final-newline": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", @@ -15923,17 +8504,11 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/sudo-prompt": { - "version": "9.2.1", - "resolved": "https://registry.npmjs.org/sudo-prompt/-/sudo-prompt-9.2.1.tgz", - "integrity": "sha512-Mu7R0g4ig9TUuGSxJavny5Rv0egCEtpZRNMrZaYS1vxkiIxGiGUwoezU3LazIQ+KE04hTrTfNPgxU5gzi7F5Pw==", - "dev": true, - "peer": true - }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, "dependencies": { "has-flag": "^4.0.0" }, @@ -15953,15 +8528,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/symbol-observable": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.2.0.tgz", - "integrity": "sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/tapable": { "version": "0.1.10", "resolved": "https://registry.npmjs.org/tapable/-/tapable-0.1.10.tgz", @@ -15995,30 +8561,6 @@ "tcomb": "^3.0.0" } }, - "node_modules/temp": { - "version": "0.8.3", - "resolved": "https://registry.npmjs.org/temp/-/temp-0.8.3.tgz", - "integrity": "sha512-jtnWJs6B1cZlHs9wPG7BrowKxZw/rf6+UpGAkr8AaYmiTyTO7zQlLoST8zx/8TcUPnZmeBoB+H8ARuHZaSijVw==", - "dev": true, - "engines": [ - "node >=0.8.0" - ], - "peer": true, - "dependencies": { - "os-tmpdir": "^1.0.0", - "rimraf": "~2.2.6" - } - }, - "node_modules/temp/node_modules/rimraf": { - "version": "2.2.8", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.2.8.tgz", - "integrity": "sha512-R5KMKHnPAQaZMqLOsyuyUmcIjSeDm+73eoqQpaXA7AZ22BL+6C+1mcUscgOsNd8WVlJuvlgAPsegcx7pjlV0Dg==", - "dev": true, - "peer": true, - "bin": { - "rimraf": "bin.js" - } - }, "node_modules/terser": { "version": "5.16.6", "resolved": "https://registry.npmjs.org/terser/-/terser-5.16.6.tgz", @@ -16105,13 +8647,6 @@ "node": ">=0.2.6" } }, - "node_modules/throat": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/throat/-/throat-5.0.0.tgz", - "integrity": "sha512-fcwX4mndzpLQKBS1DVYhGAcYaYt7vsHNIvQV+WXMvnow5cgjPphq5CaayLaGsjRdSCKZFNGt7/GYAuXaNOiYCA==", - "dev": true, - "peer": true - }, "node_modules/throttleit": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/throttleit/-/throttleit-1.0.0.tgz", @@ -16124,17 +8659,6 @@ "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", "dev": true }, - "node_modules/through2": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", - "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", - "dev": true, - "peer": true, - "dependencies": { - "readable-stream": "~2.3.6", - "xtend": "~4.0.1" - } - }, "node_modules/throwback": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/throwback/-/throwback-4.1.0.tgz", @@ -16151,9 +8675,9 @@ } }, "node_modules/timezones.json": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/timezones.json/-/timezones.json-1.7.0.tgz", - "integrity": "sha512-qzreOP+2sNmx6S3Cys0pEyI6t4qX0VPb4oFYMePUrYvQeBszGazzNpvWuAAGENGmpw7GoAND255NTkEQeXryvg==", + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/timezones.json/-/timezones.json-1.7.1.tgz", + "integrity": "sha512-4dB58ulcrRWfiGufzlofLG45RIoalCTZiFUc7tnj0g8za0CpNTyIOVlspg1JD7OFyDeW5up3ntlkukizwB0IJA==", "dev": true }, "node_modules/tmp": { @@ -16183,13 +8707,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/tmpl": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", - "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", - "dev": true, - "peer": true - }, "node_modules/to-fast-properties": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", @@ -16200,52 +8717,11 @@ "node": ">=4" } }, - "node_modules/to-object-path": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz", - "integrity": "sha512-9mWHdnGRuh3onocaHzukyvCZhzvr6tiflAy/JRFXcJX0TjgfWA9pk9t8CMbzmBE4Jfw58pXbkngtBtqYxzNEyg==", - "dev": true, - "peer": true, - "dependencies": { - "kind-of": "^3.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/to-object-path/node_modules/kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", - "dev": true, - "peer": true, - "dependencies": { - "is-buffer": "^1.1.5" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/to-regex": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/to-regex/-/to-regex-3.0.2.tgz", - "integrity": "sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==", - "dev": true, - "peer": true, - "dependencies": { - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "regex-not": "^1.0.2", - "safe-regex": "^1.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, "dependencies": { "is-number": "^7.0.0" }, @@ -16263,24 +8739,40 @@ } }, "node_modules/tough-cookie": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", - "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.3.tgz", + "integrity": "sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw==", "dev": true, "dependencies": { - "psl": "^1.1.28", - "punycode": "^2.1.1" + "psl": "^1.1.33", + "punycode": "^2.1.1", + "universalify": "^0.2.0", + "url-parse": "^1.5.3" }, "engines": { - "node": ">=0.8" + "node": ">=6" } }, - "node_modules/tr46": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "node_modules/tough-cookie/node_modules/universalify": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", + "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", "dev": true, - "peer": true + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/ts-api-utils": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.0.3.tgz", + "integrity": "sha512-wNMeqtMz5NtwpT/UZGY5alT+VoKdSsOOP/kqHFcUW1P/VRhH2wJ48+DN2WwUliNbQ976ETwDL0Ifd2VVvgonvg==", + "dev": true, + "engines": { + "node": ">=16.13.0" + }, + "peerDependencies": { + "typescript": ">=4.2.0" + } }, "node_modules/ts-expect": { "version": "1.3.0", @@ -16312,34 +8804,6 @@ "json5": "lib/cli.js" } }, - "node_modules/tslib": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", - "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==", - "dev": true, - "peer": true - }, - "node_modules/tsutils": { - "version": "3.21.0", - "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", - "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", - "dev": true, - "dependencies": { - "tslib": "^1.8.1" - }, - "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" - } - }, - "node_modules/tsutils/node_modules/tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", - "dev": true - }, "node_modules/tunnel-agent": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", @@ -16404,6 +8868,57 @@ "node": ">= 0.6" } }, + "node_modules/typed-array-buffer": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.0.tgz", + "integrity": "sha512-Y8KTSIglk9OZEr8zywiIHG/kmQ7KWyjseXs1CbSo8vC42w7hg2HgYTxSWwP0+is7bWDc1H+Fo026CpHFwm8tkw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.2.1", + "is-typed-array": "^1.1.10" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typed-array-byte-length": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.0.tgz", + "integrity": "sha512-Or/+kvLxNpeQ9DtSydonMxCx+9ZXOswtwJn17SNLvhptaXYDJvkFFP5zbfU/uLmvnBJlI4yrnXRxpdWH/M5tNA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "for-each": "^0.3.3", + "has-proto": "^1.0.1", + "is-typed-array": "^1.1.10" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-byte-offset": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.0.tgz", + "integrity": "sha512-RD97prjEt9EL8YgAgpOkf3O4IF9lhJFr9g0htQkm0rchFp/Vx7LW5Q8fSXXub7BXAODyUQohRMyOc3faCPd0hg==", + "dev": true, + "dependencies": { + "available-typed-arrays": "^1.0.5", + "call-bind": "^1.0.2", + "for-each": "^0.3.3", + "has-proto": "^1.0.1", + "is-typed-array": "^1.1.10" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/typed-array-length": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.4.tgz", @@ -16419,51 +8934,16 @@ } }, "node_modules/typescript": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.0.4.tgz", - "integrity": "sha512-cW9T5W9xY37cc+jfEnaUvX91foxtHkza3Nw3wkoF4sSlKn0MONdkdEndig/qPBWXNkmplh3NzayQzCiHM4/hqw==", + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz", + "integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==", "dev": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" }, "engines": { - "node": ">=12.20" - } - }, - "node_modules/uglify-es": { - "version": "3.3.9", - "resolved": "https://registry.npmjs.org/uglify-es/-/uglify-es-3.3.9.tgz", - "integrity": "sha512-r+MU0rfv4L/0eeW3xZrd16t4NZfK8Ld4SWVglYBb7ez5uXFWHuVRs6xCTrf1yirs9a4j4Y27nn7SRfO6v67XsQ==", - "deprecated": "support for ECMAScript is superseded by `uglify-js` as of v3.13.0", - "dev": true, - "peer": true, - "dependencies": { - "commander": "~2.13.0", - "source-map": "~0.6.1" - }, - "bin": { - "uglifyjs": "bin/uglifyjs" - }, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/uglify-es/node_modules/commander": { - "version": "2.13.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.13.0.tgz", - "integrity": "sha512-MVuS359B+YzaWqjCL/c+22gfryv+mCBPHAv3zyVI2GN8EY6IRP8VwtasXn8jyyhvvq84R4ImN1OKRtcbIasjYA==", - "dev": true, - "peer": true - }, - "node_modules/uglify-es/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "peer": true, - "engines": { - "node": ">=0.10.0" + "node": ">=14.17" } }, "node_modules/unbox-primitive": { @@ -16481,80 +8961,11 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/unicode-canonical-property-names-ecmascript": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz", - "integrity": "sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ==", - "dev": true, - "peer": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/unicode-match-property-ecmascript": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz", - "integrity": "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==", - "dev": true, - "peer": true, - "dependencies": { - "unicode-canonical-property-names-ecmascript": "^2.0.0", - "unicode-property-aliases-ecmascript": "^2.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/unicode-match-property-value-ecmascript": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.1.0.tgz", - "integrity": "sha512-qxkjQt6qjg/mYscYMC0XKRn3Rh0wFPlfxB0xkt9CfyTvpX1Ra0+rAmdX2QyAobptSEvuy4RtpPRui6XkV+8wjA==", - "dev": true, - "peer": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/unicode-property-aliases-ecmascript": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz", - "integrity": "sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==", - "dev": true, - "peer": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/union-value": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz", - "integrity": "sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg==", - "dev": true, - "peer": true, - "dependencies": { - "arr-union": "^3.1.0", - "get-value": "^2.0.6", - "is-extendable": "^0.1.1", - "set-value": "^2.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/union-value/node_modules/is-extendable": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", - "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", - "dev": true, - "peer": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/universalify": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", + "dev": true, "engines": { "node": ">= 10.0.0" } @@ -16568,58 +8979,6 @@ "node": ">= 0.8" } }, - "node_modules/unset-value": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz", - "integrity": "sha512-PcA2tsuGSF9cnySLHTLSh2qrQiJ70mn+r+Glzxv2TWZblxsxCC52BDlZoPCsz7STd9pN7EZetkWZBAvk4cgZdQ==", - "dev": true, - "peer": true, - "dependencies": { - "has-value": "^0.3.1", - "isobject": "^3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/unset-value/node_modules/has-value": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/has-value/-/has-value-0.3.1.tgz", - "integrity": "sha512-gpG936j8/MzaeID5Yif+577c17TxaDmhuyVgSwtnL/q8UUTySg8Mecb+8Cf1otgLoD7DDH75axp86ER7LFsf3Q==", - "dev": true, - "peer": true, - "dependencies": { - "get-value": "^2.0.3", - "has-values": "^0.1.4", - "isobject": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/unset-value/node_modules/has-value/node_modules/isobject": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", - "integrity": "sha512-+OUdGJlgjOBZDfxnDjYYG6zp487z0JGNQq3cYQYg5f5hKR+syHMsaztzGeml/4kGG55CSpKSpWTY+jYGgsHLgA==", - "dev": true, - "peer": true, - "dependencies": { - "isarray": "1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/unset-value/node_modules/has-values": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/has-values/-/has-values-0.1.4.tgz", - "integrity": "sha512-J8S0cEdWuQbqD9//tlZxiMuMNmxB8PlEwvYwuxsTmR1G5RXUePEX/SJn7aD0GMLieuZYSwNH0cQuJGwnYunXRQ==", - "dev": true, - "peer": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/untildify": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/untildify/-/untildify-4.0.0.tgz", @@ -16665,14 +9024,6 @@ "punycode": "^2.1.0" } }, - "node_modules/urix": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz", - "integrity": "sha512-Am1ousAhSLBeB9cG/7k7r2R0zj50uDRlZHPGbazid5s9rlF1F/QKYObEKSIunSjIOkJZqwRRLpvewjEkM7pSqg==", - "deprecated": "Please see https://github.com/lydell/urix#deprecated", - "dev": true, - "peer": true - }, "node_modules/url": { "version": "0.10.3", "resolved": "https://registry.npmjs.org/url/-/url-0.10.3.tgz", @@ -16683,32 +9034,22 @@ "querystring": "0.2.0" } }, + "node_modules/url-parse": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "dev": true, + "dependencies": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, "node_modules/url/node_modules/punycode": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=", "dev": true }, - "node_modules/use": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz", - "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==", - "dev": true, - "peer": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/use-sync-external-store": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz", - "integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==", - "dev": true, - "peer": true, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0" - } - }, "node_modules/util": { "version": "0.10.4", "resolved": "https://registry.npmjs.org/util/-/util-0.10.4.tgz", @@ -16740,10 +9081,14 @@ } }, "node_modules/uuid": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz", - "integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==", + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", "dev": true, + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], "bin": { "uuid": "dist/bin/uuid" } @@ -16780,23 +9125,6 @@ "extsprintf": "^1.2.0" } }, - "node_modules/vlq": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/vlq/-/vlq-1.0.1.tgz", - "integrity": "sha512-gQpnTgkubC6hQgdIcRdYGDSDc+SaujOdyesZQMv6JlfQee/9Mp0Qhnys6WxDWvQnL5WZdT7o2Ul187aSt0Rq+w==", - "dev": true, - "peer": true - }, - "node_modules/walker": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", - "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", - "dev": true, - "peer": true, - "dependencies": { - "makeerror": "1.0.12" - } - }, "node_modules/watchpack": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz", @@ -16811,23 +9139,6 @@ "node": ">=10.13.0" } }, - "node_modules/wcwidth": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", - "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==", - "dev": true, - "peer": true, - "dependencies": { - "defaults": "^1.0.3" - } - }, - "node_modules/webidl-conversions": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", - "dev": true, - "peer": true - }, "node_modules/webpack": { "version": "5.76.1", "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.76.1.tgz", @@ -16937,28 +9248,11 @@ "node": ">=6" } }, - "node_modules/whatwg-fetch": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.2.tgz", - "integrity": "sha512-bJlen0FcuU/0EMLrdbJ7zOnW6ITZLrZMIarMUVmdKtsGvZna8vxKYaexICWPfZ8qwf9fzNq+UEIZrnSaApt6RA==", - "dev": true, - "peer": true - }, - "node_modules/whatwg-url": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", - "dev": true, - "peer": true, - "dependencies": { - "tr46": "~0.0.3", - "webidl-conversions": "^3.0.0" - } - }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, "dependencies": { "isexe": "^2.0.0" }, @@ -16985,6 +9279,38 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/which-builtin-type": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.1.3.tgz", + "integrity": "sha512-YmjsSMDBYsM1CaFiayOVT06+KJeXf0o5M/CAd4o1lTadFAtacTUM49zoYxr/oroopFDfhvN6iEcBxUyc3gvKmw==", + "dev": true, + "dependencies": { + "function.prototype.name": "^1.1.5", + "has-tostringtag": "^1.0.0", + "is-async-function": "^2.0.0", + "is-date-object": "^1.0.5", + "is-finalizationregistry": "^1.0.2", + "is-generator-function": "^1.0.10", + "is-regex": "^1.1.4", + "is-weakref": "^1.0.2", + "isarray": "^2.0.5", + "which-boxed-primitive": "^1.0.2", + "which-collection": "^1.0.1", + "which-typed-array": "^1.1.9" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-builtin-type/node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true + }, "node_modules/which-collection": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.1.tgz", @@ -17007,17 +9333,16 @@ "dev": true }, "node_modules/which-typed-array": { - "version": "1.1.9", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.9.tgz", - "integrity": "sha512-w9c4xkx6mPidwp7180ckYWfMmvxpjlZuIudNtDf4N/tTAUB8VJbX25qZoAsrtGuYNnGw3pa0AXgbGKRB8/EceA==", + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.11.tgz", + "integrity": "sha512-qe9UWWpkeG5yzZ0tNYxDmd7vo58HDBc39mZ0xWWpolAGADdFOzkfamWLDxkOWcvHQKVmdTyQdLD4NOfjLWTKew==", "dev": true, "dependencies": { "available-typed-arrays": "^1.0.5", "call-bind": "^1.0.2", "for-each": "^0.3.3", "gopd": "^1.0.1", - "has-tostringtag": "^1.0.0", - "is-typed-array": "^1.1.10" + "has-tostringtag": "^1.0.0" }, "engines": { "node": ">= 0.4" @@ -17026,15 +9351,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/word-wrap": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", - "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/workerpool": { "version": "6.2.1", "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.2.1.tgz", @@ -17073,29 +9389,8 @@ "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" - }, - "node_modules/write-file-atomic": { - "version": "2.4.3", - "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-2.4.3.tgz", - "integrity": "sha512-GaETH5wwsX+GcnzhPgKcKjJ6M2Cq3/iZp1WyY/X1CSqrW+jVNM9Y7D8EC2sM4ZG/V8wZlSniJnCKWPmBYAucRQ==", - "dev": true, - "peer": true, - "dependencies": { - "graceful-fs": "^4.1.11", - "imurmurhash": "^0.1.4", - "signal-exit": "^3.0.2" - } - }, - "node_modules/ws": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ws/-/ws-6.2.2.tgz", - "integrity": "sha512-zmhltoSR8u1cnDsD43TX59mzoMZsLKqUweyYBAIvTngR3shc0W6aOZylZmq/7hqyVxPdi+5Ud2QInblgyE72fw==", - "dev": true, - "peer": true, - "dependencies": { - "async-limiter": "~1.0.0" - } + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true }, "node_modules/xml": { "version": "1.0.1", @@ -17149,14 +9444,6 @@ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", "dev": true }, - "node_modules/yaml": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.2.2.tgz", - "integrity": "sha512-CBKFWExMn46Foo4cldiChEzn7S7SRV+wqiluAb6xmueD/fGyRHIhX8m14vVGgeFWjN540nKCNVj6P21eQjgTuA==", - "engines": { - "node": ">= 14" - } - }, "node_modules/yargs": { "version": "17.7.2", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", @@ -17258,6 +9545,12 @@ } }, "dependencies": { + "@aashutoshrathi/word-wrap": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", + "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==", + "dev": true + }, "@ampproject/remapping": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.0.tgz", @@ -17310,14 +9603,14 @@ } }, "@babel/eslint-parser": { - "version": "7.21.8", - "resolved": "https://registry.npmjs.org/@babel/eslint-parser/-/eslint-parser-7.21.8.tgz", - "integrity": "sha512-HLhI+2q+BP3sf78mFUZNCGc10KEmoUqtUT1OCdMZsN+qr4qFeLUod62/zAnF3jNQstwyasDkZnVXwfK2Bml7MQ==", + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/eslint-parser/-/eslint-parser-7.22.15.tgz", + "integrity": "sha512-yc8OOBIQk1EcRrpizuARSQS0TWAcOMpEJ1aafhNznaeYkeL+OhqnDObGFylB8ka8VFF/sZc+S4RzHyO+3LjQxg==", "dev": true, "requires": { "@nicolo-ribaudo/eslint-scope-5-internals": "5.1.1-v1", "eslint-visitor-keys": "^2.1.0", - "semver": "^6.3.0" + "semver": "^6.3.1" }, "dependencies": { "eslint-visitor-keys": { @@ -17325,13 +9618,19 @@ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", "dev": true + }, + "semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true } } }, "@babel/eslint-plugin": { - "version": "7.19.1", - "resolved": "https://registry.npmjs.org/@babel/eslint-plugin/-/eslint-plugin-7.19.1.tgz", - "integrity": "sha512-ElGPkQPapKMa3zVqXHkZYzuL7I5LbRw9UWBUArgWsdWDDb9XcACqOpBib5tRPA9XvbVZYrFUkoQPbiJ4BFvu4w==", + "version": "7.22.10", + "resolved": "https://registry.npmjs.org/@babel/eslint-plugin/-/eslint-plugin-7.22.10.tgz", + "integrity": "sha512-SRZcvo3fnO5h79B9DZSV6LG2vHH7OWsSNp1huFLHsXKyytRG413byQk9zxW1VcPOhnzfx2VIUz+8aGbiE7fOkA==", "dev": true, "requires": { "eslint-rule-composer": "^0.3.0" @@ -17364,27 +9663,6 @@ } } }, - "@babel/helper-annotate-as-pure": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.18.6.tgz", - "integrity": "sha512-duORpUiYrEpzKIop6iNbjnwKLAKnJ47csTyRACyEmWj0QdUrm5aqNJGHSSEQSUAvNW0ojX0dOmK9dZduvkfeXA==", - "dev": true, - "peer": true, - "requires": { - "@babel/types": "^7.18.6" - } - }, - "@babel/helper-builder-binary-assignment-operator-visitor": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.18.6.tgz", - "integrity": "sha512-KT10c1oWEpmrIRYnthbzHgoOf6B+Xd6a5yhdbNtdhtG7aO1or5HViuf1TQR36xY/QprXA5nvxO6nAjhJ4y38jw==", - "dev": true, - "peer": true, - "requires": { - "@babel/helper-explode-assignable-expression": "^7.18.6", - "@babel/types": "^7.18.6" - } - }, "@babel/helper-compilation-targets": { "version": "7.20.7", "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.20.7.tgz", @@ -17418,48 +9696,6 @@ } } }, - "@babel/helper-create-class-features-plugin": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.18.6.tgz", - "integrity": "sha512-YfDzdnoxHGV8CzqHGyCbFvXg5QESPFkXlHtvdCkesLjjVMT2Adxe4FGUR5ChIb3DxSaXO12iIOCWoXdsUVwnqw==", - "dev": true, - "peer": true, - "requires": { - "@babel/helper-annotate-as-pure": "^7.18.6", - "@babel/helper-environment-visitor": "^7.18.6", - "@babel/helper-function-name": "^7.18.6", - "@babel/helper-member-expression-to-functions": "^7.18.6", - "@babel/helper-optimise-call-expression": "^7.18.6", - "@babel/helper-replace-supers": "^7.18.6", - "@babel/helper-split-export-declaration": "^7.18.6" - } - }, - "@babel/helper-create-regexp-features-plugin": { - "version": "7.21.0", - "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.21.0.tgz", - "integrity": "sha512-N+LaFW/auRSWdx7SHD/HiARwXQju1vXTW4fKr4u5SgBUTm51OKEjKgj+cs00ggW3kEvNqwErnlwuq7Y3xBe4eg==", - "dev": true, - "peer": true, - "requires": { - "@babel/helper-annotate-as-pure": "^7.18.6", - "regexpu-core": "^5.3.1" - } - }, - "@babel/helper-define-polyfill-provider": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.3.3.tgz", - "integrity": "sha512-z5aQKU4IzbqCC1XH0nAqfsFLMVSo22SBKUc0BxGrLkolTdPTructy0ToNnlO2zA4j9Q/7pjMZf0DSY+DSTYzww==", - "dev": true, - "peer": true, - "requires": { - "@babel/helper-compilation-targets": "^7.17.7", - "@babel/helper-plugin-utils": "^7.16.7", - "debug": "^4.1.1", - "lodash.debounce": "^4.0.8", - "resolve": "^1.14.2", - "semver": "^6.1.2" - } - }, "@babel/helper-environment-visitor": { "version": "7.18.9", "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.9.tgz", @@ -17467,16 +9703,6 @@ "dev": true, "peer": true }, - "@babel/helper-explode-assignable-expression": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.18.6.tgz", - "integrity": "sha512-eyAYAsQmB80jNfg4baAtLeWAQHfHFiR483rzFK+BhETlGZaQC9bsfrugfXDCbRHLQbIA7U5NxhhOxN7p/dWIcg==", - "dev": true, - "peer": true, - "requires": { - "@babel/types": "^7.18.6" - } - }, "@babel/helper-function-name": { "version": "7.21.0", "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.21.0.tgz", @@ -17498,16 +9724,6 @@ "@babel/types": "^7.18.6" } }, - "@babel/helper-member-expression-to-functions": { - "version": "7.21.0", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.21.0.tgz", - "integrity": "sha512-Muu8cdZwNN6mRRNG6lAYErJ5X3bRevgYR2O8wN0yn7jJSnGDu6eG59RfT29JHxGUovyfrh6Pj0XzmR7drNVL3Q==", - "dev": true, - "peer": true, - "requires": { - "@babel/types": "^7.21.0" - } - }, "@babel/helper-module-imports": { "version": "7.18.6", "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.18.6.tgz", @@ -17535,51 +9751,6 @@ "@babel/types": "^7.21.2" } }, - "@babel/helper-optimise-call-expression": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.18.6.tgz", - "integrity": "sha512-HP59oD9/fEHQkdcbgFCnbmgH5vIQTJbxh2yf+CdM89/glUNnuzr87Q8GIjGEnOktTROemO0Pe0iPAYbqZuOUiA==", - "dev": true, - "peer": true, - "requires": { - "@babel/types": "^7.18.6" - } - }, - "@babel/helper-plugin-utils": { - "version": "7.20.2", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.20.2.tgz", - "integrity": "sha512-8RvlJG2mj4huQ4pZ+rU9lqKi9ZKiRmuvGuM2HlWmkmgOhbs6zEAw6IEiJ5cQqGbDzGZOhwuOQNtZMi/ENLjZoQ==", - "dev": true, - "peer": true - }, - "@babel/helper-remap-async-to-generator": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.18.9.tgz", - "integrity": "sha512-dI7q50YKd8BAv3VEfgg7PS7yD3Rtbi2J1XMXaalXO0W0164hYLnh8zpjRS0mte9MfVp/tltvr/cfdXPvJr1opA==", - "dev": true, - "peer": true, - "requires": { - "@babel/helper-annotate-as-pure": "^7.18.6", - "@babel/helper-environment-visitor": "^7.18.9", - "@babel/helper-wrap-function": "^7.18.9", - "@babel/types": "^7.18.9" - } - }, - "@babel/helper-replace-supers": { - "version": "7.20.7", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.20.7.tgz", - "integrity": "sha512-vujDMtB6LVfNW13jhlCrp48QNslK6JXi7lQG736HVbHz/mbf4Dc7tIRh1Xf5C0rF7BP8iiSxGMCmY6Ci1ven3A==", - "dev": true, - "peer": true, - "requires": { - "@babel/helper-environment-visitor": "^7.18.9", - "@babel/helper-member-expression-to-functions": "^7.20.7", - "@babel/helper-optimise-call-expression": "^7.18.6", - "@babel/template": "^7.20.7", - "@babel/traverse": "^7.20.7", - "@babel/types": "^7.20.7" - } - }, "@babel/helper-simple-access": { "version": "7.20.2", "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.20.2.tgz", @@ -17590,16 +9761,6 @@ "@babel/types": "^7.20.2" } }, - "@babel/helper-skip-transparent-expression-wrappers": { - "version": "7.20.0", - "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.20.0.tgz", - "integrity": "sha512-5y1JYeNKfvnT8sZcK9DVRtpTbGiomYIHviSP3OQWmDPU3DeH4a1ZlT/N2lyQ5P8egjcRaT/Y9aNqUxK0WsnIIg==", - "dev": true, - "peer": true, - "requires": { - "@babel/types": "^7.20.0" - } - }, "@babel/helper-split-export-declaration": { "version": "7.18.6", "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.18.6.tgz", @@ -17630,19 +9791,6 @@ "dev": true, "peer": true }, - "@babel/helper-wrap-function": { - "version": "7.20.5", - "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.20.5.tgz", - "integrity": "sha512-bYMxIWK5mh+TgXGVqAtnu5Yn1un+v8DDZtqyzKRLUzrh70Eal2O3aZ7aPYiMADO4uKlkzOiRiZ6GX5q3qxvW9Q==", - "dev": true, - "peer": true, - "requires": { - "@babel/helper-function-name": "^7.19.0", - "@babel/template": "^7.18.10", - "@babel/traverse": "^7.20.5", - "@babel/types": "^7.20.5" - } - }, "@babel/helpers": { "version": "7.21.0", "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.21.0.tgz", @@ -17725,998 +9873,6 @@ "dev": true, "peer": true }, - "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.18.6.tgz", - "integrity": "sha512-Dgxsyg54Fx1d4Nge8UnvTrED63vrwOdPmyvPzlNN/boaliRP54pm3pGzZD1SJUwrBA+Cs/xdG8kXX6Mn/RfISQ==", - "dev": true, - "peer": true, - "requires": { - "@babel/helper-plugin-utils": "^7.18.6" - } - }, - "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { - "version": "7.20.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.20.7.tgz", - "integrity": "sha512-sbr9+wNE5aXMBBFBICk01tt7sBf2Oc9ikRFEcem/ZORup9IMUdNhW7/wVLEbbtlWOsEubJet46mHAL2C8+2jKQ==", - "dev": true, - "peer": true, - "requires": { - "@babel/helper-plugin-utils": "^7.20.2", - "@babel/helper-skip-transparent-expression-wrappers": "^7.20.0", - "@babel/plugin-proposal-optional-chaining": "^7.20.7" - } - }, - "@babel/plugin-proposal-async-generator-functions": { - "version": "7.20.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.20.7.tgz", - "integrity": "sha512-xMbiLsn/8RK7Wq7VeVytytS2L6qE69bXPB10YCmMdDZbKF4okCqY74pI/jJQ/8U0b/F6NrT2+14b8/P9/3AMGA==", - "dev": true, - "peer": true, - "requires": { - "@babel/helper-environment-visitor": "^7.18.9", - "@babel/helper-plugin-utils": "^7.20.2", - "@babel/helper-remap-async-to-generator": "^7.18.9", - "@babel/plugin-syntax-async-generators": "^7.8.4" - } - }, - "@babel/plugin-proposal-class-properties": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.18.6.tgz", - "integrity": "sha512-cumfXOF0+nzZrrN8Rf0t7M+tF6sZc7vhQwYQck9q1/5w2OExlD+b4v4RpMJFaV1Z7WcDRgO6FqvxqxGlwo+RHQ==", - "dev": true, - "peer": true, - "requires": { - "@babel/helper-create-class-features-plugin": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6" - } - }, - "@babel/plugin-proposal-class-static-block": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-static-block/-/plugin-proposal-class-static-block-7.18.6.tgz", - "integrity": "sha512-+I3oIiNxrCpup3Gi8n5IGMwj0gOCAjcJUSQEcotNnCCPMEnixawOQ+KeJPlgfjzx+FKQ1QSyZOWe7wmoJp7vhw==", - "dev": true, - "peer": true, - "requires": { - "@babel/helper-create-class-features-plugin": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6", - "@babel/plugin-syntax-class-static-block": "^7.14.5" - } - }, - "@babel/plugin-proposal-dynamic-import": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.18.6.tgz", - "integrity": "sha512-1auuwmK+Rz13SJj36R+jqFPMJWyKEDd7lLSdOj4oJK0UTgGueSAtkrCvz9ewmgyU/P941Rv2fQwZJN8s6QruXw==", - "dev": true, - "peer": true, - "requires": { - "@babel/helper-plugin-utils": "^7.18.6", - "@babel/plugin-syntax-dynamic-import": "^7.8.3" - } - }, - "@babel/plugin-proposal-export-default-from": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-export-default-from/-/plugin-proposal-export-default-from-7.18.6.tgz", - "integrity": "sha512-oTvzWB16T9cB4j5kX8c8DuUHo/4QtR2P9vnUNKed9xqFP8Jos/IRniz1FiIryn6luDYoltDJSYF7RCpbm2doMg==", - "dev": true, - "peer": true, - "requires": { - "@babel/helper-plugin-utils": "^7.18.6", - "@babel/plugin-syntax-export-default-from": "^7.18.6" - } - }, - "@babel/plugin-proposal-export-namespace-from": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-export-namespace-from/-/plugin-proposal-export-namespace-from-7.18.9.tgz", - "integrity": "sha512-k1NtHyOMvlDDFeb9G5PhUXuGj8m/wiwojgQVEhJ/fsVsMCpLyOP4h0uGEjYJKrRI+EVPlb5Jk+Gt9P97lOGwtA==", - "dev": true, - "peer": true, - "requires": { - "@babel/helper-plugin-utils": "^7.18.9", - "@babel/plugin-syntax-export-namespace-from": "^7.8.3" - } - }, - "@babel/plugin-proposal-json-strings": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.18.6.tgz", - "integrity": "sha512-lr1peyn9kOdbYc0xr0OdHTZ5FMqS6Di+H0Fz2I/JwMzGmzJETNeOFq2pBySw6X/KFL5EWDjlJuMsUGRFb8fQgQ==", - "dev": true, - "peer": true, - "requires": { - "@babel/helper-plugin-utils": "^7.18.6", - "@babel/plugin-syntax-json-strings": "^7.8.3" - } - }, - "@babel/plugin-proposal-logical-assignment-operators": { - "version": "7.20.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-logical-assignment-operators/-/plugin-proposal-logical-assignment-operators-7.20.7.tgz", - "integrity": "sha512-y7C7cZgpMIjWlKE5T7eJwp+tnRYM89HmRvWM5EQuB5BoHEONjmQ8lSNmBUwOyy/GFRsohJED51YBF79hE1djug==", - "dev": true, - "peer": true, - "requires": { - "@babel/helper-plugin-utils": "^7.20.2", - "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4" - } - }, - "@babel/plugin-proposal-nullish-coalescing-operator": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.18.6.tgz", - "integrity": "sha512-wQxQzxYeJqHcfppzBDnm1yAY0jSRkUXR2z8RePZYrKwMKgMlE8+Z6LUno+bd6LvbGh8Gltvy74+9pIYkr+XkKA==", - "dev": true, - "peer": true, - "requires": { - "@babel/helper-plugin-utils": "^7.18.6", - "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3" - } - }, - "@babel/plugin-proposal-numeric-separator": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.18.6.tgz", - "integrity": "sha512-ozlZFogPqoLm8WBr5Z8UckIoE4YQ5KESVcNudyXOR8uqIkliTEgJ3RoketfG6pmzLdeZF0H/wjE9/cCEitBl7Q==", - "dev": true, - "peer": true, - "requires": { - "@babel/helper-plugin-utils": "^7.18.6", - "@babel/plugin-syntax-numeric-separator": "^7.10.4" - } - }, - "@babel/plugin-proposal-object-rest-spread": { - "version": "7.20.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.20.7.tgz", - "integrity": "sha512-d2S98yCiLxDVmBmE8UjGcfPvNEUbA1U5q5WxaWFUGRzJSVAZqm5W6MbPct0jxnegUZ0niLeNX+IOzEs7wYg9Dg==", - "dev": true, - "peer": true, - "requires": { - "@babel/compat-data": "^7.20.5", - "@babel/helper-compilation-targets": "^7.20.7", - "@babel/helper-plugin-utils": "^7.20.2", - "@babel/plugin-syntax-object-rest-spread": "^7.8.3", - "@babel/plugin-transform-parameters": "^7.20.7" - } - }, - "@babel/plugin-proposal-optional-catch-binding": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.18.6.tgz", - "integrity": "sha512-Q40HEhs9DJQyaZfUjjn6vE8Cv4GmMHCYuMGIWUnlxH6400VGxOuwWsPt4FxXxJkC/5eOzgn0z21M9gMT4MOhbw==", - "dev": true, - "peer": true, - "requires": { - "@babel/helper-plugin-utils": "^7.18.6", - "@babel/plugin-syntax-optional-catch-binding": "^7.8.3" - } - }, - "@babel/plugin-proposal-optional-chaining": { - "version": "7.21.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.21.0.tgz", - "integrity": "sha512-p4zeefM72gpmEe2fkUr/OnOXpWEf8nAgk7ZYVqqfFiyIG7oFfVZcCrU64hWn5xp4tQ9LkV4bTIa5rD0KANpKNA==", - "dev": true, - "peer": true, - "requires": { - "@babel/helper-plugin-utils": "^7.20.2", - "@babel/helper-skip-transparent-expression-wrappers": "^7.20.0", - "@babel/plugin-syntax-optional-chaining": "^7.8.3" - } - }, - "@babel/plugin-proposal-private-methods": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-methods/-/plugin-proposal-private-methods-7.18.6.tgz", - "integrity": "sha512-nutsvktDItsNn4rpGItSNV2sz1XwS+nfU0Rg8aCx3W3NOKVzdMjJRu0O5OkgDp3ZGICSTbgRpxZoWsxoKRvbeA==", - "dev": true, - "peer": true, - "requires": { - "@babel/helper-create-class-features-plugin": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6" - } - }, - "@babel/plugin-proposal-private-property-in-object": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.18.6.tgz", - "integrity": "sha512-9Rysx7FOctvT5ouj5JODjAFAkgGoudQuLPamZb0v1TGLpapdNaftzifU8NTWQm0IRjqoYypdrSmyWgkocDQ8Dw==", - "dev": true, - "peer": true, - "requires": { - "@babel/helper-annotate-as-pure": "^7.18.6", - "@babel/helper-create-class-features-plugin": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6", - "@babel/plugin-syntax-private-property-in-object": "^7.14.5" - } - }, - "@babel/plugin-proposal-unicode-property-regex": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.18.6.tgz", - "integrity": "sha512-2BShG/d5yoZyXZfVePH91urL5wTG6ASZU9M4o03lKK8u8UW1y08OMttBSOADTcJrnPMpvDXRG3G8fyLh4ovs8w==", - "dev": true, - "peer": true, - "requires": { - "@babel/helper-create-regexp-features-plugin": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6" - } - }, - "@babel/plugin-syntax-async-generators": { - "version": "7.8.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", - "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", - "dev": true, - "peer": true, - "requires": { - "@babel/helper-plugin-utils": "^7.8.0" - } - }, - "@babel/plugin-syntax-class-properties": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", - "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", - "dev": true, - "peer": true, - "requires": { - "@babel/helper-plugin-utils": "^7.12.13" - } - }, - "@babel/plugin-syntax-class-static-block": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", - "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", - "dev": true, - "peer": true, - "requires": { - "@babel/helper-plugin-utils": "^7.14.5" - } - }, - "@babel/plugin-syntax-dynamic-import": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz", - "integrity": "sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==", - "dev": true, - "peer": true, - "requires": { - "@babel/helper-plugin-utils": "^7.8.0" - } - }, - "@babel/plugin-syntax-export-default-from": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-export-default-from/-/plugin-syntax-export-default-from-7.18.6.tgz", - "integrity": "sha512-Kr//z3ujSVNx6E9z9ih5xXXMqK07VVTuqPmqGe6Mss/zW5XPeLZeSDZoP9ab/hT4wPKqAgjl2PnhPrcpk8Seew==", - "dev": true, - "peer": true, - "requires": { - "@babel/helper-plugin-utils": "^7.18.6" - } - }, - "@babel/plugin-syntax-export-namespace-from": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.8.3.tgz", - "integrity": "sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q==", - "dev": true, - "peer": true, - "requires": { - "@babel/helper-plugin-utils": "^7.8.3" - } - }, - "@babel/plugin-syntax-flow": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-flow/-/plugin-syntax-flow-7.18.6.tgz", - "integrity": "sha512-LUbR+KNTBWCUAqRG9ex5Gnzu2IOkt8jRJbHHXFT9q+L9zm7M/QQbEqXyw1n1pohYvOyWC8CjeyjrSaIwiYjK7A==", - "dev": true, - "peer": true, - "requires": { - "@babel/helper-plugin-utils": "^7.18.6" - } - }, - "@babel/plugin-syntax-import-assertions": { - "version": "7.20.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.20.0.tgz", - "integrity": "sha512-IUh1vakzNoWalR8ch/areW7qFopR2AEw03JlG7BbrDqmQ4X3q9uuipQwSGrUn7oGiemKjtSLDhNtQHzMHr1JdQ==", - "dev": true, - "peer": true, - "requires": { - "@babel/helper-plugin-utils": "^7.19.0" - } - }, - "@babel/plugin-syntax-json-strings": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", - "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", - "dev": true, - "peer": true, - "requires": { - "@babel/helper-plugin-utils": "^7.8.0" - } - }, - "@babel/plugin-syntax-jsx": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.18.6.tgz", - "integrity": "sha512-6mmljtAedFGTWu2p/8WIORGwy+61PLgOMPOdazc7YoJ9ZCWUyFy3A6CpPkRKLKD1ToAesxX8KGEViAiLo9N+7Q==", - "dev": true, - "peer": true, - "requires": { - "@babel/helper-plugin-utils": "^7.18.6" - } - }, - "@babel/plugin-syntax-logical-assignment-operators": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", - "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", - "dev": true, - "peer": true, - "requires": { - "@babel/helper-plugin-utils": "^7.10.4" - } - }, - "@babel/plugin-syntax-nullish-coalescing-operator": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", - "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", - "dev": true, - "peer": true, - "requires": { - "@babel/helper-plugin-utils": "^7.8.0" - } - }, - "@babel/plugin-syntax-numeric-separator": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", - "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", - "dev": true, - "peer": true, - "requires": { - "@babel/helper-plugin-utils": "^7.10.4" - } - }, - "@babel/plugin-syntax-object-rest-spread": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", - "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", - "dev": true, - "peer": true, - "requires": { - "@babel/helper-plugin-utils": "^7.8.0" - } - }, - "@babel/plugin-syntax-optional-catch-binding": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", - "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", - "dev": true, - "peer": true, - "requires": { - "@babel/helper-plugin-utils": "^7.8.0" - } - }, - "@babel/plugin-syntax-optional-chaining": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", - "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", - "dev": true, - "peer": true, - "requires": { - "@babel/helper-plugin-utils": "^7.8.0" - } - }, - "@babel/plugin-syntax-private-property-in-object": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", - "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", - "dev": true, - "peer": true, - "requires": { - "@babel/helper-plugin-utils": "^7.14.5" - } - }, - "@babel/plugin-syntax-top-level-await": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", - "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", - "dev": true, - "peer": true, - "requires": { - "@babel/helper-plugin-utils": "^7.14.5" - } - }, - "@babel/plugin-syntax-typescript": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.18.6.tgz", - "integrity": "sha512-mAWAuq4rvOepWCBid55JuRNvpTNf2UGVgoz4JV0fXEKolsVZDzsa4NqCef758WZJj/GDu0gVGItjKFiClTAmZA==", - "dev": true, - "peer": true, - "requires": { - "@babel/helper-plugin-utils": "^7.18.6" - } - }, - "@babel/plugin-transform-arrow-functions": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.18.6.tgz", - "integrity": "sha512-9S9X9RUefzrsHZmKMbDXxweEH+YlE8JJEuat9FdvW9Qh1cw7W64jELCtWNkPBPX5En45uy28KGvA/AySqUh8CQ==", - "dev": true, - "peer": true, - "requires": { - "@babel/helper-plugin-utils": "^7.18.6" - } - }, - "@babel/plugin-transform-async-to-generator": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.18.6.tgz", - "integrity": "sha512-ARE5wZLKnTgPW7/1ftQmSi1CmkqqHo2DNmtztFhvgtOWSDfq0Cq9/9L+KnZNYSNrydBekhW3rwShduf59RoXag==", - "dev": true, - "peer": true, - "requires": { - "@babel/helper-module-imports": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6", - "@babel/helper-remap-async-to-generator": "^7.18.6" - } - }, - "@babel/plugin-transform-block-scoped-functions": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.18.6.tgz", - "integrity": "sha512-ExUcOqpPWnliRcPqves5HJcJOvHvIIWfuS4sroBUenPuMdmW+SMHDakmtS7qOo13sVppmUijqeTv7qqGsvURpQ==", - "dev": true, - "peer": true, - "requires": { - "@babel/helper-plugin-utils": "^7.18.6" - } - }, - "@babel/plugin-transform-block-scoping": { - "version": "7.21.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.21.0.tgz", - "integrity": "sha512-Mdrbunoh9SxwFZapeHVrwFmri16+oYotcZysSzhNIVDwIAb1UV+kvnxULSYq9J3/q5MDG+4X6w8QVgD1zhBXNQ==", - "dev": true, - "peer": true, - "requires": { - "@babel/helper-plugin-utils": "^7.20.2" - } - }, - "@babel/plugin-transform-classes": { - "version": "7.21.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.21.0.tgz", - "integrity": "sha512-RZhbYTCEUAe6ntPehC4hlslPWosNHDox+vAs4On/mCLRLfoDVHf6hVEd7kuxr1RnHwJmxFfUM3cZiZRmPxJPXQ==", - "dev": true, - "peer": true, - "requires": { - "@babel/helper-annotate-as-pure": "^7.18.6", - "@babel/helper-compilation-targets": "^7.20.7", - "@babel/helper-environment-visitor": "^7.18.9", - "@babel/helper-function-name": "^7.21.0", - "@babel/helper-optimise-call-expression": "^7.18.6", - "@babel/helper-plugin-utils": "^7.20.2", - "@babel/helper-replace-supers": "^7.20.7", - "@babel/helper-split-export-declaration": "^7.18.6", - "globals": "^11.1.0" - } - }, - "@babel/plugin-transform-computed-properties": { - "version": "7.20.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.20.7.tgz", - "integrity": "sha512-Lz7MvBK6DTjElHAmfu6bfANzKcxpyNPeYBGEafyA6E5HtRpjpZwU+u7Qrgz/2OR0z+5TvKYbPdphfSaAcZBrYQ==", - "dev": true, - "peer": true, - "requires": { - "@babel/helper-plugin-utils": "^7.20.2", - "@babel/template": "^7.20.7" - } - }, - "@babel/plugin-transform-destructuring": { - "version": "7.21.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.21.3.tgz", - "integrity": "sha512-bp6hwMFzuiE4HqYEyoGJ/V2LeIWn+hLVKc4pnj++E5XQptwhtcGmSayM029d/j2X1bPKGTlsyPwAubuU22KhMA==", - "dev": true, - "peer": true, - "requires": { - "@babel/helper-plugin-utils": "^7.20.2" - } - }, - "@babel/plugin-transform-dotall-regex": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.18.6.tgz", - "integrity": "sha512-6S3jpun1eEbAxq7TdjLotAsl4WpQI9DxfkycRcKrjhQYzU87qpXdknpBg/e+TdcMehqGnLFi7tnFUBR02Vq6wg==", - "dev": true, - "peer": true, - "requires": { - "@babel/helper-create-regexp-features-plugin": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6" - } - }, - "@babel/plugin-transform-duplicate-keys": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.18.9.tgz", - "integrity": "sha512-d2bmXCtZXYc59/0SanQKbiWINadaJXqtvIQIzd4+hNwkWBgyCd5F/2t1kXoUdvPMrxzPvhK6EMQRROxsue+mfw==", - "dev": true, - "peer": true, - "requires": { - "@babel/helper-plugin-utils": "^7.18.9" - } - }, - "@babel/plugin-transform-exponentiation-operator": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.18.6.tgz", - "integrity": "sha512-wzEtc0+2c88FVR34aQmiz56dxEkxr2g8DQb/KfaFa1JYXOFVsbhvAonFN6PwVWj++fKmku8NP80plJ5Et4wqHw==", - "dev": true, - "peer": true, - "requires": { - "@babel/helper-builder-binary-assignment-operator-visitor": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6" - } - }, - "@babel/plugin-transform-flow-strip-types": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-flow-strip-types/-/plugin-transform-flow-strip-types-7.18.6.tgz", - "integrity": "sha512-wE0xtA7csz+hw4fKPwxmu5jnzAsXPIO57XnRwzXP3T19jWh1BODnPGoG9xKYwvAwusP7iUktHayRFbMPGtODaQ==", - "dev": true, - "peer": true, - "requires": { - "@babel/helper-plugin-utils": "^7.18.6", - "@babel/plugin-syntax-flow": "^7.18.6" - } - }, - "@babel/plugin-transform-for-of": { - "version": "7.18.8", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.18.8.tgz", - "integrity": "sha512-yEfTRnjuskWYo0k1mHUqrVWaZwrdq8AYbfrpqULOJOaucGSp4mNMVps+YtA8byoevxS/urwU75vyhQIxcCgiBQ==", - "dev": true, - "peer": true, - "requires": { - "@babel/helper-plugin-utils": "^7.18.6" - } - }, - "@babel/plugin-transform-function-name": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.18.9.tgz", - "integrity": "sha512-WvIBoRPaJQ5yVHzcnJFor7oS5Ls0PYixlTYE63lCj2RtdQEl15M68FXQlxnG6wdraJIXRdR7KI+hQ7q/9QjrCQ==", - "dev": true, - "peer": true, - "requires": { - "@babel/helper-compilation-targets": "^7.18.9", - "@babel/helper-function-name": "^7.18.9", - "@babel/helper-plugin-utils": "^7.18.9" - } - }, - "@babel/plugin-transform-literals": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.18.9.tgz", - "integrity": "sha512-IFQDSRoTPnrAIrI5zoZv73IFeZu2dhu6irxQjY9rNjTT53VmKg9fenjvoiOWOkJ6mm4jKVPtdMzBY98Fp4Z4cg==", - "dev": true, - "peer": true, - "requires": { - "@babel/helper-plugin-utils": "^7.18.9" - } - }, - "@babel/plugin-transform-member-expression-literals": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.18.6.tgz", - "integrity": "sha512-qSF1ihLGO3q+/g48k85tUjD033C29TNTVB2paCwZPVmOsjn9pClvYYrM2VeJpBY2bcNkuny0YUyTNRyRxJ54KA==", - "dev": true, - "peer": true, - "requires": { - "@babel/helper-plugin-utils": "^7.18.6" - } - }, - "@babel/plugin-transform-modules-amd": { - "version": "7.20.11", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.20.11.tgz", - "integrity": "sha512-NuzCt5IIYOW0O30UvqktzHYR2ud5bOWbY0yaxWZ6G+aFzOMJvrs5YHNikrbdaT15+KNO31nPOy5Fim3ku6Zb5g==", - "dev": true, - "peer": true, - "requires": { - "@babel/helper-module-transforms": "^7.20.11", - "@babel/helper-plugin-utils": "^7.20.2" - } - }, - "@babel/plugin-transform-modules-commonjs": { - "version": "7.21.2", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.21.2.tgz", - "integrity": "sha512-Cln+Yy04Gxua7iPdj6nOV96smLGjpElir5YwzF0LBPKoPlLDNJePNlrGGaybAJkd0zKRnOVXOgizSqPYMNYkzA==", - "dev": true, - "peer": true, - "requires": { - "@babel/helper-module-transforms": "^7.21.2", - "@babel/helper-plugin-utils": "^7.20.2", - "@babel/helper-simple-access": "^7.20.2" - } - }, - "@babel/plugin-transform-modules-systemjs": { - "version": "7.20.11", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.20.11.tgz", - "integrity": "sha512-vVu5g9BPQKSFEmvt2TA4Da5N+QVS66EX21d8uoOihC+OCpUoGvzVsXeqFdtAEfVa5BILAeFt+U7yVmLbQnAJmw==", - "dev": true, - "peer": true, - "requires": { - "@babel/helper-hoist-variables": "^7.18.6", - "@babel/helper-module-transforms": "^7.20.11", - "@babel/helper-plugin-utils": "^7.20.2", - "@babel/helper-validator-identifier": "^7.19.1" - } - }, - "@babel/plugin-transform-modules-umd": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.18.6.tgz", - "integrity": "sha512-dcegErExVeXcRqNtkRU/z8WlBLnvD4MRnHgNs3MytRO1Mn1sHRyhbcpYbVMGclAqOjdW+9cfkdZno9dFdfKLfQ==", - "dev": true, - "peer": true, - "requires": { - "@babel/helper-module-transforms": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6" - } - }, - "@babel/plugin-transform-named-capturing-groups-regex": { - "version": "7.20.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.20.5.tgz", - "integrity": "sha512-mOW4tTzi5iTLnw+78iEq3gr8Aoq4WNRGpmSlrogqaiCBoR1HFhpU4JkpQFOHfeYx3ReVIFWOQJS4aZBRvuZ6mA==", - "dev": true, - "peer": true, - "requires": { - "@babel/helper-create-regexp-features-plugin": "^7.20.5", - "@babel/helper-plugin-utils": "^7.20.2" - } - }, - "@babel/plugin-transform-new-target": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.18.6.tgz", - "integrity": "sha512-DjwFA/9Iu3Z+vrAn+8pBUGcjhxKguSMlsFqeCKbhb9BAV756v0krzVK04CRDi/4aqmk8BsHb4a/gFcaA5joXRw==", - "dev": true, - "peer": true, - "requires": { - "@babel/helper-plugin-utils": "^7.18.6" - } - }, - "@babel/plugin-transform-object-super": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.18.6.tgz", - "integrity": "sha512-uvGz6zk+pZoS1aTZrOvrbj6Pp/kK2mp45t2B+bTDre2UgsZZ8EZLSJtUg7m/no0zOJUWgFONpB7Zv9W2tSaFlA==", - "dev": true, - "peer": true, - "requires": { - "@babel/helper-plugin-utils": "^7.18.6", - "@babel/helper-replace-supers": "^7.18.6" - } - }, - "@babel/plugin-transform-parameters": { - "version": "7.21.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.21.3.tgz", - "integrity": "sha512-Wxc+TvppQG9xWFYatvCGPvZ6+SIUxQ2ZdiBP+PHYMIjnPXD+uThCshaz4NZOnODAtBjjcVQQ/3OKs9LW28purQ==", - "dev": true, - "peer": true, - "requires": { - "@babel/helper-plugin-utils": "^7.20.2" - } - }, - "@babel/plugin-transform-property-literals": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.18.6.tgz", - "integrity": "sha512-cYcs6qlgafTud3PAzrrRNbQtfpQ8+y/+M5tKmksS9+M1ckbH6kzY8MrexEM9mcA6JDsukE19iIRvAyYl463sMg==", - "dev": true, - "peer": true, - "requires": { - "@babel/helper-plugin-utils": "^7.18.6" - } - }, - "@babel/plugin-transform-react-display-name": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.18.6.tgz", - "integrity": "sha512-TV4sQ+T013n61uMoygyMRm+xf04Bd5oqFpv2jAEQwSZ8NwQA7zeRPg1LMVg2PWi3zWBz+CLKD+v5bcpZ/BS0aA==", - "dev": true, - "peer": true, - "requires": { - "@babel/helper-plugin-utils": "^7.18.6" - } - }, - "@babel/plugin-transform-react-jsx": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.18.6.tgz", - "integrity": "sha512-Mz7xMPxoy9kPS/JScj6fJs03TZ/fZ1dJPlMjRAgTaxaS0fUBk8FV/A2rRgfPsVCZqALNwMexD+0Uaf5zlcKPpw==", - "dev": true, - "peer": true, - "requires": { - "@babel/helper-annotate-as-pure": "^7.18.6", - "@babel/helper-module-imports": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6", - "@babel/plugin-syntax-jsx": "^7.18.6", - "@babel/types": "^7.18.6" - } - }, - "@babel/plugin-transform-react-jsx-self": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.18.6.tgz", - "integrity": "sha512-A0LQGx4+4Jv7u/tWzoJF7alZwnBDQd6cGLh9P+Ttk4dpiL+J5p7NSNv/9tlEFFJDq3kjxOavWmbm6t0Gk+A3Ig==", - "dev": true, - "peer": true, - "requires": { - "@babel/helper-plugin-utils": "^7.18.6" - } - }, - "@babel/plugin-transform-react-jsx-source": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.18.6.tgz", - "integrity": "sha512-utZmlASneDfdaMh0m/WausbjUjEdGrQJz0vFK93d7wD3xf5wBtX219+q6IlCNZeguIcxS2f/CvLZrlLSvSHQXw==", - "dev": true, - "peer": true, - "requires": { - "@babel/helper-plugin-utils": "^7.18.6" - } - }, - "@babel/plugin-transform-regenerator": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.18.6.tgz", - "integrity": "sha512-poqRI2+qiSdeldcz4wTSTXBRryoq3Gc70ye7m7UD5Ww0nE29IXqMl6r7Nd15WBgRd74vloEMlShtH6CKxVzfmQ==", - "dev": true, - "peer": true, - "requires": { - "@babel/helper-plugin-utils": "^7.18.6", - "regenerator-transform": "^0.15.0" - } - }, - "@babel/plugin-transform-reserved-words": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.18.6.tgz", - "integrity": "sha512-oX/4MyMoypzHjFrT1CdivfKZ+XvIPMFXwwxHp/r0Ddy2Vuomt4HDFGmft1TAY2yiTKiNSsh3kjBAzcM8kSdsjA==", - "dev": true, - "peer": true, - "requires": { - "@babel/helper-plugin-utils": "^7.18.6" - } - }, - "@babel/plugin-transform-runtime": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.18.6.tgz", - "integrity": "sha512-8uRHk9ZmRSnWqUgyae249EJZ94b0yAGLBIqzZzl+0iEdbno55Pmlt/32JZsHwXD9k/uZj18Aqqk35wBX4CBTXA==", - "dev": true, - "peer": true, - "requires": { - "@babel/helper-module-imports": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6", - "babel-plugin-polyfill-corejs2": "^0.3.1", - "babel-plugin-polyfill-corejs3": "^0.5.2", - "babel-plugin-polyfill-regenerator": "^0.3.1", - "semver": "^6.3.0" - } - }, - "@babel/plugin-transform-shorthand-properties": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.18.6.tgz", - "integrity": "sha512-eCLXXJqv8okzg86ywZJbRn19YJHU4XUa55oz2wbHhaQVn/MM+XhukiT7SYqp/7o00dg52Rj51Ny+Ecw4oyoygw==", - "dev": true, - "peer": true, - "requires": { - "@babel/helper-plugin-utils": "^7.18.6" - } - }, - "@babel/plugin-transform-spread": { - "version": "7.20.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.20.7.tgz", - "integrity": "sha512-ewBbHQ+1U/VnH1fxltbJqDeWBU1oNLG8Dj11uIv3xVf7nrQu0bPGe5Rf716r7K5Qz+SqtAOVswoVunoiBtGhxw==", - "dev": true, - "peer": true, - "requires": { - "@babel/helper-plugin-utils": "^7.20.2", - "@babel/helper-skip-transparent-expression-wrappers": "^7.20.0" - } - }, - "@babel/plugin-transform-sticky-regex": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.18.6.tgz", - "integrity": "sha512-kfiDrDQ+PBsQDO85yj1icueWMfGfJFKN1KCkndygtu/C9+XUfydLC8Iv5UYJqRwy4zk8EcplRxEOeLyjq1gm6Q==", - "dev": true, - "peer": true, - "requires": { - "@babel/helper-plugin-utils": "^7.18.6" - } - }, - "@babel/plugin-transform-template-literals": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.18.9.tgz", - "integrity": "sha512-S8cOWfT82gTezpYOiVaGHrCbhlHgKhQt8XH5ES46P2XWmX92yisoZywf5km75wv5sYcXDUCLMmMxOLCtthDgMA==", - "dev": true, - "peer": true, - "requires": { - "@babel/helper-plugin-utils": "^7.18.9" - } - }, - "@babel/plugin-transform-typeof-symbol": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.18.9.tgz", - "integrity": "sha512-SRfwTtF11G2aemAZWivL7PD+C9z52v9EvMqH9BuYbabyPuKUvSWks3oCg6041pT925L4zVFqaVBeECwsmlguEw==", - "dev": true, - "peer": true, - "requires": { - "@babel/helper-plugin-utils": "^7.18.9" - } - }, - "@babel/plugin-transform-typescript": { - "version": "7.18.8", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.18.8.tgz", - "integrity": "sha512-p2xM8HI83UObjsZGofMV/EdYjamsDm6MoN3hXPYIT0+gxIoopE+B7rPYKAxfrz9K9PK7JafTTjqYC6qipLExYA==", - "dev": true, - "peer": true, - "requires": { - "@babel/helper-create-class-features-plugin": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6", - "@babel/plugin-syntax-typescript": "^7.18.6" - } - }, - "@babel/plugin-transform-unicode-escapes": { - "version": "7.18.10", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.18.10.tgz", - "integrity": "sha512-kKAdAI+YzPgGY/ftStBFXTI1LZFju38rYThnfMykS+IXy8BVx+res7s2fxf1l8I35DV2T97ezo6+SGrXz6B3iQ==", - "dev": true, - "peer": true, - "requires": { - "@babel/helper-plugin-utils": "^7.18.9" - } - }, - "@babel/plugin-transform-unicode-regex": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.18.6.tgz", - "integrity": "sha512-gE7A6Lt7YLnNOL3Pb9BNeZvi+d8l7tcRrG4+pwJjK9hD2xX4mEvjlQW60G9EEmfXVYRPv9VRQcyegIVHCql/AA==", - "dev": true, - "peer": true, - "requires": { - "@babel/helper-create-regexp-features-plugin": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6" - } - }, - "@babel/preset-env": { - "version": "7.20.2", - "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.20.2.tgz", - "integrity": "sha512-1G0efQEWR1EHkKvKHqbG+IN/QdgwfByUpM5V5QroDzGV2t3S/WXNQd693cHiHTlCFMpr9B6FkPFXDA2lQcKoDg==", - "dev": true, - "peer": true, - "requires": { - "@babel/compat-data": "^7.20.1", - "@babel/helper-compilation-targets": "^7.20.0", - "@babel/helper-plugin-utils": "^7.20.2", - "@babel/helper-validator-option": "^7.18.6", - "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.18.6", - "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.18.9", - "@babel/plugin-proposal-async-generator-functions": "^7.20.1", - "@babel/plugin-proposal-class-properties": "^7.18.6", - "@babel/plugin-proposal-class-static-block": "^7.18.6", - "@babel/plugin-proposal-dynamic-import": "^7.18.6", - "@babel/plugin-proposal-export-namespace-from": "^7.18.9", - "@babel/plugin-proposal-json-strings": "^7.18.6", - "@babel/plugin-proposal-logical-assignment-operators": "^7.18.9", - "@babel/plugin-proposal-nullish-coalescing-operator": "^7.18.6", - "@babel/plugin-proposal-numeric-separator": "^7.18.6", - "@babel/plugin-proposal-object-rest-spread": "^7.20.2", - "@babel/plugin-proposal-optional-catch-binding": "^7.18.6", - "@babel/plugin-proposal-optional-chaining": "^7.18.9", - "@babel/plugin-proposal-private-methods": "^7.18.6", - "@babel/plugin-proposal-private-property-in-object": "^7.18.6", - "@babel/plugin-proposal-unicode-property-regex": "^7.18.6", - "@babel/plugin-syntax-async-generators": "^7.8.4", - "@babel/plugin-syntax-class-properties": "^7.12.13", - "@babel/plugin-syntax-class-static-block": "^7.14.5", - "@babel/plugin-syntax-dynamic-import": "^7.8.3", - "@babel/plugin-syntax-export-namespace-from": "^7.8.3", - "@babel/plugin-syntax-import-assertions": "^7.20.0", - "@babel/plugin-syntax-json-strings": "^7.8.3", - "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", - "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", - "@babel/plugin-syntax-numeric-separator": "^7.10.4", - "@babel/plugin-syntax-object-rest-spread": "^7.8.3", - "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", - "@babel/plugin-syntax-optional-chaining": "^7.8.3", - "@babel/plugin-syntax-private-property-in-object": "^7.14.5", - "@babel/plugin-syntax-top-level-await": "^7.14.5", - "@babel/plugin-transform-arrow-functions": "^7.18.6", - "@babel/plugin-transform-async-to-generator": "^7.18.6", - "@babel/plugin-transform-block-scoped-functions": "^7.18.6", - "@babel/plugin-transform-block-scoping": "^7.20.2", - "@babel/plugin-transform-classes": "^7.20.2", - "@babel/plugin-transform-computed-properties": "^7.18.9", - "@babel/plugin-transform-destructuring": "^7.20.2", - "@babel/plugin-transform-dotall-regex": "^7.18.6", - "@babel/plugin-transform-duplicate-keys": "^7.18.9", - "@babel/plugin-transform-exponentiation-operator": "^7.18.6", - "@babel/plugin-transform-for-of": "^7.18.8", - "@babel/plugin-transform-function-name": "^7.18.9", - "@babel/plugin-transform-literals": "^7.18.9", - "@babel/plugin-transform-member-expression-literals": "^7.18.6", - "@babel/plugin-transform-modules-amd": "^7.19.6", - "@babel/plugin-transform-modules-commonjs": "^7.19.6", - "@babel/plugin-transform-modules-systemjs": "^7.19.6", - "@babel/plugin-transform-modules-umd": "^7.18.6", - "@babel/plugin-transform-named-capturing-groups-regex": "^7.19.1", - "@babel/plugin-transform-new-target": "^7.18.6", - "@babel/plugin-transform-object-super": "^7.18.6", - "@babel/plugin-transform-parameters": "^7.20.1", - "@babel/plugin-transform-property-literals": "^7.18.6", - "@babel/plugin-transform-regenerator": "^7.18.6", - "@babel/plugin-transform-reserved-words": "^7.18.6", - "@babel/plugin-transform-shorthand-properties": "^7.18.6", - "@babel/plugin-transform-spread": "^7.19.0", - "@babel/plugin-transform-sticky-regex": "^7.18.6", - "@babel/plugin-transform-template-literals": "^7.18.9", - "@babel/plugin-transform-typeof-symbol": "^7.18.9", - "@babel/plugin-transform-unicode-escapes": "^7.18.10", - "@babel/plugin-transform-unicode-regex": "^7.18.6", - "@babel/preset-modules": "^0.1.5", - "@babel/types": "^7.20.2", - "babel-plugin-polyfill-corejs2": "^0.3.3", - "babel-plugin-polyfill-corejs3": "^0.6.0", - "babel-plugin-polyfill-regenerator": "^0.4.1", - "core-js-compat": "^3.25.1", - "semver": "^6.3.0" - }, - "dependencies": { - "babel-plugin-polyfill-corejs3": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.6.0.tgz", - "integrity": "sha512-+eHqR6OPcBhJOGgsIar7xoAB1GcSwVUA3XjAd7HJNzOXT4wv6/H7KIdA/Nc60cvUlDbKApmqNvD1B1bzOt4nyA==", - "dev": true, - "peer": true, - "requires": { - "@babel/helper-define-polyfill-provider": "^0.3.3", - "core-js-compat": "^3.25.1" - } - }, - "babel-plugin-polyfill-regenerator": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.4.1.tgz", - "integrity": "sha512-NtQGmyQDXjQqQ+IzRkBVwEOz9lQ4zxAQZgoAYEtU9dJjnl1Oc98qnN7jcp+bE7O7aYzVpavXE3/VKXNzUbh7aw==", - "dev": true, - "peer": true, - "requires": { - "@babel/helper-define-polyfill-provider": "^0.3.3" - } - } - } - }, - "@babel/preset-flow": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/preset-flow/-/preset-flow-7.18.6.tgz", - "integrity": "sha512-E7BDhL64W6OUqpuyHnSroLnqyRTcG6ZdOBl1OKI/QK/HJfplqK/S3sq1Cckx7oTodJ5yOXyfw7rEADJ6UjoQDQ==", - "dev": true, - "peer": true, - "requires": { - "@babel/helper-plugin-utils": "^7.18.6", - "@babel/helper-validator-option": "^7.18.6", - "@babel/plugin-transform-flow-strip-types": "^7.18.6" - } - }, - "@babel/preset-modules": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.5.tgz", - "integrity": "sha512-A57th6YRG7oR3cq/yt/Y84MvGgE0eJG2F1JLhKuyG+jFxEgrd/HAMJatiFtmOiZurz+0DkrvbheCLaV5f2JfjA==", - "dev": true, - "peer": true, - "requires": { - "@babel/helper-plugin-utils": "^7.0.0", - "@babel/plugin-proposal-unicode-property-regex": "^7.4.4", - "@babel/plugin-transform-dotall-regex": "^7.4.4", - "@babel/types": "^7.4.4", - "esutils": "^2.0.2" - } - }, - "@babel/preset-typescript": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.18.6.tgz", - "integrity": "sha512-s9ik86kXBAnD760aybBucdpnLsAt0jK1xqJn2juOn9lkOvSHV60os5hxoVJsPzMQxvnUJFAlkont2DvvaYEBtQ==", - "dev": true, - "peer": true, - "requires": { - "@babel/helper-plugin-utils": "^7.18.6", - "@babel/helper-validator-option": "^7.18.6", - "@babel/plugin-transform-typescript": "^7.18.6" - } - }, - "@babel/register": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/register/-/register-7.18.6.tgz", - "integrity": "sha512-tkYtONzaO8rQubZzpBnvZPFcHgh8D9F55IjOsYton4X2IBoyRn2ZSWQqySTZnUn2guZbxbQiAB27hJEbvXamhQ==", - "dev": true, - "peer": true, - "requires": { - "clone-deep": "^4.0.1", - "find-cache-dir": "^2.0.0", - "make-dir": "^2.1.0", - "pirates": "^4.0.5", - "source-map-support": "^0.5.16" - } - }, - "@babel/regjsgen": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/@babel/regjsgen/-/regjsgen-0.8.0.tgz", - "integrity": "sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA==", - "dev": true, - "peer": true - }, "@babel/runtime": { "version": "7.21.0", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.21.0.tgz", @@ -18777,9 +9933,9 @@ "optional": true }, "@cypress/request": { - "version": "2.88.11", - "resolved": "https://registry.npmjs.org/@cypress/request/-/request-2.88.11.tgz", - "integrity": "sha512-M83/wfQ1EkspjkE2lNWNV5ui2Cv7UCv1swW1DqljahbzLVWltcsexQh8jYtuS/vzFXP+HySntGM83ZXA9fn17w==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@cypress/request/-/request-3.0.1.tgz", + "integrity": "sha512-TWivJlJi8ZDx2wGOw1dbLuHJKUYX7bWySw377nlnGOW3hP9/MUKIsEdXT/YngWxVdgNCHRBmFlBipE+5/2ZZlQ==", "dev": true, "requires": { "aws-sign2": "~0.7.0", @@ -18795,9 +9951,9 @@ "json-stringify-safe": "~5.0.1", "mime-types": "~2.1.19", "performance-now": "^2.1.0", - "qs": "~6.10.3", + "qs": "6.10.4", "safe-buffer": "^5.1.2", - "tough-cookie": "~2.5.0", + "tough-cookie": "^4.1.3", "tunnel-agent": "^0.6.0", "uuid": "^8.3.2" }, @@ -18841,20 +9997,20 @@ } }, "@eslint-community/regexpp": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.4.0.tgz", - "integrity": "sha512-A9983Q0LnDGdLPjxyXQ00sbV+K+O+ko2Dr+CZigbHWtX9pNfxlaBkMR8X1CztI73zuEyEBXTVjx7CE+/VSwDiQ==", + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.8.1.tgz", + "integrity": "sha512-PWiOzLIUAjN/w5K17PoF4n6sKBw0gqLHPhywmYHP4t1VFQQVYeb1yWsJwnMVEMl3tUHME7X/SJPZLmtG7XBDxQ==", "dev": true }, "@eslint/eslintrc": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.0.2.tgz", - "integrity": "sha512-3W4f5tDUra+pA+FzgugqL2pRimUTDJWKr7BINqOpkZrC0uYI0NIc0/JFgBROCU07HR6GieA5m3/rsPIhDmCXTQ==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.2.tgz", + "integrity": "sha512-+wvgpDsrB1YqAMdEUCcnTlpfVBH7Vqn6A/NT3D8WVXFIaKMlErPIZT3oCIAVCOtarRpMtelZLqJeU3t7WY6X6g==", "dev": true, "requires": { "ajv": "^6.12.4", "debug": "^4.3.2", - "espree": "^9.5.1", + "espree": "^9.6.0", "globals": "^13.19.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", @@ -18864,9 +10020,9 @@ }, "dependencies": { "globals": { - "version": "13.20.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.20.0.tgz", - "integrity": "sha512-Qg5QtVkCy/kv3FUSlu4ukeZDVf9ee0iXLAUYX13gbR17bnejFTzr4iS9bY7kwCf1NztRNm1t91fjOiyx4CSwPQ==", + "version": "13.21.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.21.0.tgz", + "integrity": "sha512-ybyme3s4yy/t/3s35bewwXKOf7cvzfreG2lH0lZl0JB7I4GxRP2ghxOK/Nb9EkRXdbBXZLfq/p/0W2JUONB/Gg==", "dev": true, "requires": { "type-fest": "^0.20.2" @@ -18897,32 +10053,15 @@ } }, "@eslint/js": { - "version": "8.39.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.39.0.tgz", - "integrity": "sha512-kf9RB0Fg7NZfap83B3QOqOGg9QmD9yBudqQXzzOtn3i4y7ZUXe5ONeW34Gwi+TxhH4mvj72R1Zc300KUMa9Bng==", + "version": "8.50.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.50.0.tgz", + "integrity": "sha512-NCC3zz2+nvYd+Ckfh87rA47zfu2QsQpvc6k1yzTk+b9KzRj0wkGa8LSoGOXN6Zv4lRf/EIoZ80biDh9HOI+RNQ==", "dev": true }, - "@hapi/hoek": { - "version": "9.3.0", - "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz", - "integrity": "sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==", - "dev": true, - "peer": true - }, - "@hapi/topo": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-5.1.0.tgz", - "integrity": "sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==", - "dev": true, - "peer": true, - "requires": { - "@hapi/hoek": "^9.0.0" - } - }, "@humanwhocodes/config-array": { - "version": "0.11.8", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.8.tgz", - "integrity": "sha512-UybHIJzJnR5Qc/MsD9Kr+RpO2h+/P1GhOwdiLPXK5TWk5sgTdu88bTD9UP+CKbPPh5Rni1u0GjAdYQLemG8g+g==", + "version": "0.11.11", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.11.tgz", + "integrity": "sha512-N2brEuAadi0CcdeMXUkhbZB84eskAc8MEX1By6qEchoVywSgXPIjou4rYsl0V3Hj0ZnuGycGCjdNgockbzeWNA==", "dev": true, "requires": { "@humanwhocodes/object-schema": "^1.2.1", @@ -18942,30 +10081,6 @@ "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", "dev": true }, - "@jest/create-cache-key-function": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/@jest/create-cache-key-function/-/create-cache-key-function-27.5.1.tgz", - "integrity": "sha512-dmH1yW+makpTSURTy8VzdUwFnfQh1G8R+DxO2Ho2FFmBbKFEVm+3jWdvFhE2VqB/LATCTokkP0dotjyQyw5/AQ==", - "dev": true, - "peer": true, - "requires": { - "@jest/types": "^27.5.1" - } - }, - "@jest/types": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-27.5.1.tgz", - "integrity": "sha512-Cx46iJ9QpwQTjIdq5VJu2QTMMs3QlEjI0x1QbBP5W1+nMzyc2XmimiRR/CbX9TO0cPTeUlxWMOu8mslYsJ8DEw==", - "dev": true, - "peer": true, - "requires": { - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^16.0.0", - "chalk": "^4.0.0" - } - }, "@jridgewell/gen-mapping": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.1.1.tgz", @@ -19034,10 +10149,32 @@ "@jridgewell/sourcemap-codec": "1.4.14" } }, + "@mattermost/client": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/@mattermost/client/-/client-9.0.0.tgz", + "integrity": "sha512-hXeOKvqUtuOC7/iFrGigDwKGvXsxdyLJgveU5SOwxQsi0fIf13D1a3IZH8xt6Df6icXp14yIVKD20t1UuHIc1g==", + "dev": true, + "requires": { + "form-data": "^4.0.0" + }, + "dependencies": { + "form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dev": true, + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + } + } + } + }, "@mattermost/types": { - "version": "7.10.0", - "resolved": "https://registry.npmjs.org/@mattermost/types/-/types-7.10.0.tgz", - "integrity": "sha512-nPJThc+BHIUGmqN5GMTCbFiETxxT7pD7hoEM3N+z3l6bRrdTcLaK2NVmbSjEhi6jN1jDoFtD9ieCiCPg5pT4EA==", + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/@mattermost/types/-/types-9.0.0.tgz", + "integrity": "sha512-pRqls7j3/oEQQEhZYv6p7yfqN8k+XMRvFIOyeiy3ov0Vwcm7JTRCH8qT+XmqTigVItvVRiUyzpdN9JWj2Rb5yQ==", "dev": true, "requires": {} }, @@ -19076,954 +10213,44 @@ "fastq": "^1.6.0" } }, - "@react-native-community/cli": { - "version": "8.0.3", - "resolved": "https://registry.npmjs.org/@react-native-community/cli/-/cli-8.0.3.tgz", - "integrity": "sha512-7gY7QCEdpYDbvbdZBt6w64YPExLoiUpH/lVRaR4tKl6JalqXzrUotOJnBOS/qEC4q0nk0WXsiC5EkuiSliKS5Q==", - "dev": true, - "peer": true, - "requires": { - "@react-native-community/cli-clean": "^8.0.0", - "@react-native-community/cli-config": "^8.0.3", - "@react-native-community/cli-debugger-ui": "^8.0.0", - "@react-native-community/cli-doctor": "^8.0.3", - "@react-native-community/cli-hermes": "^8.0.2", - "@react-native-community/cli-plugin-metro": "^8.0.0", - "@react-native-community/cli-server-api": "^8.0.0", - "@react-native-community/cli-tools": "^8.0.0", - "@react-native-community/cli-types": "^8.0.0", - "chalk": "^4.1.2", - "commander": "^2.19.0", - "execa": "^1.0.0", - "find-up": "^4.1.0", - "fs-extra": "^8.1.0", - "graceful-fs": "^4.1.3", - "leven": "^3.1.0", - "lodash": "^4.17.15", - "minimist": "^1.2.0", - "prompts": "^2.4.0", - "semver": "^6.3.0" - }, - "dependencies": { - "commander": { - "version": "2.20.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", - "dev": true, - "peer": true - }, - "cross-spawn": { - "version": "6.0.5", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", - "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", - "dev": true, - "peer": true, - "requires": { - "nice-try": "^1.0.4", - "path-key": "^2.0.1", - "semver": "^5.5.0", - "shebang-command": "^1.2.0", - "which": "^1.2.9" - }, - "dependencies": { - "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true, - "peer": true - } - } - }, - "execa": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", - "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", - "dev": true, - "peer": true, - "requires": { - "cross-spawn": "^6.0.0", - "get-stream": "^4.0.0", - "is-stream": "^1.1.0", - "npm-run-path": "^2.0.0", - "p-finally": "^1.0.0", - "signal-exit": "^3.0.0", - "strip-eof": "^1.0.0" - } - }, - "find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, - "peer": true, - "requires": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - } - }, - "fs-extra": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", - "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", - "dev": true, - "peer": true, - "requires": { - "graceful-fs": "^4.2.0", - "jsonfile": "^4.0.0", - "universalify": "^0.1.0" - } - }, - "get-stream": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", - "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", - "dev": true, - "peer": true, - "requires": { - "pump": "^3.0.0" - } - }, - "is-stream": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", - "integrity": "sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==", - "dev": true, - "peer": true - }, - "jsonfile": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", - "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", - "dev": true, - "peer": true, - "requires": { - "graceful-fs": "^4.1.6" - } - }, - "locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dev": true, - "peer": true, - "requires": { - "p-locate": "^4.1.0" - } - }, - "npm-run-path": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", - "integrity": "sha512-lJxZYlT4DW/bRUtFh1MQIWqmLwQfAxnqWG4HhEdjMlkrJYnJn0Jrr2u3mgxqaWsdiBc76TYkTG/mhrnYTuzfHw==", - "dev": true, - "peer": true, - "requires": { - "path-key": "^2.0.0" - } - }, - "p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dev": true, - "peer": true, - "requires": { - "p-try": "^2.0.0" - } - }, - "p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dev": true, - "peer": true, - "requires": { - "p-limit": "^2.2.0" - } - }, - "path-key": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", - "integrity": "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==", - "dev": true, - "peer": true - }, - "shebang-command": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", - "integrity": "sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==", - "dev": true, - "peer": true, - "requires": { - "shebang-regex": "^1.0.0" - } - }, - "shebang-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", - "integrity": "sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==", - "dev": true, - "peer": true - }, - "universalify": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", - "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", - "dev": true, - "peer": true - }, - "which": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", - "dev": true, - "peer": true, - "requires": { - "isexe": "^2.0.0" - } - } - } - }, - "@react-native-community/cli-clean": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/@react-native-community/cli-clean/-/cli-clean-8.0.0.tgz", - "integrity": "sha512-VY/kwyH5xp6oXiB9bcwa+I9W5k6WR/nX3s85FuMW76hSlgG1UVAGL04uZPwYlSmMZuSOSuoXOaIjJ7wAvQMBpg==", - "dev": true, - "peer": true, - "requires": { - "@react-native-community/cli-tools": "^8.0.0", - "chalk": "^4.1.2", - "execa": "^1.0.0", - "prompts": "^2.4.0" - }, - "dependencies": { - "cross-spawn": { - "version": "6.0.5", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", - "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", - "dev": true, - "peer": true, - "requires": { - "nice-try": "^1.0.4", - "path-key": "^2.0.1", - "semver": "^5.5.0", - "shebang-command": "^1.2.0", - "which": "^1.2.9" - } - }, - "execa": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", - "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", - "dev": true, - "peer": true, - "requires": { - "cross-spawn": "^6.0.0", - "get-stream": "^4.0.0", - "is-stream": "^1.1.0", - "npm-run-path": "^2.0.0", - "p-finally": "^1.0.0", - "signal-exit": "^3.0.0", - "strip-eof": "^1.0.0" - } - }, - "get-stream": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", - "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", - "dev": true, - "peer": true, - "requires": { - "pump": "^3.0.0" - } - }, - "is-stream": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", - "integrity": "sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==", - "dev": true, - "peer": true - }, - "npm-run-path": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", - "integrity": "sha512-lJxZYlT4DW/bRUtFh1MQIWqmLwQfAxnqWG4HhEdjMlkrJYnJn0Jrr2u3mgxqaWsdiBc76TYkTG/mhrnYTuzfHw==", - "dev": true, - "peer": true, - "requires": { - "path-key": "^2.0.0" - } - }, - "path-key": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", - "integrity": "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==", - "dev": true, - "peer": true - }, - "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true, - "peer": true - }, - "shebang-command": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", - "integrity": "sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==", - "dev": true, - "peer": true, - "requires": { - "shebang-regex": "^1.0.0" - } - }, - "shebang-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", - "integrity": "sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==", - "dev": true, - "peer": true - }, - "which": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", - "dev": true, - "peer": true, - "requires": { - "isexe": "^2.0.0" - } - } - } - }, - "@react-native-community/cli-config": { - "version": "8.0.3", - "resolved": "https://registry.npmjs.org/@react-native-community/cli-config/-/cli-config-8.0.3.tgz", - "integrity": "sha512-QhLU6QZywkoO4FzpeEkdoYml0nE9tBwhmOUI/c5iYPOtKhhXiW8kNCLiX96TJDiZonalzptkkNiRZkipdz/8hw==", - "dev": true, - "peer": true, - "requires": { - "@react-native-community/cli-tools": "^8.0.0", - "cosmiconfig": "^5.1.0", - "deepmerge": "^3.2.0", - "glob": "^7.1.3", - "joi": "^17.2.1" - }, - "dependencies": { - "deepmerge": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-3.3.0.tgz", - "integrity": "sha512-GRQOafGHwMHpjPx9iCvTgpu9NojZ49q794EEL94JVEw6VaeA8XTUyBKvAkOOjBX9oJNiV6G3P+T+tihFjo2TqA==", - "dev": true, - "peer": true - } - } - }, - "@react-native-community/cli-debugger-ui": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/@react-native-community/cli-debugger-ui/-/cli-debugger-ui-8.0.0.tgz", - "integrity": "sha512-u2jq06GZwZ9sRERzd9FIgpW6yv4YOW4zz7Ym/B8eSzviLmy3yI/8mxJtvlGW+J8lBsfMcQoqJpqI6Rl1nZy9yQ==", - "dev": true, - "peer": true, - "requires": { - "serve-static": "^1.13.1" - } - }, - "@react-native-community/cli-doctor": { - "version": "8.0.3", - "resolved": "https://registry.npmjs.org/@react-native-community/cli-doctor/-/cli-doctor-8.0.3.tgz", - "integrity": "sha512-ndISZhZqOoeSuQCm5KLwJNkckk14Bqn1N8LHJbC6l4zAyDU0nQRO1IVPoV5uyaANMzMqSNzS6k9N4M0PpcuhIQ==", - "dev": true, - "peer": true, - "requires": { - "@react-native-community/cli-config": "^8.0.3", - "@react-native-community/cli-platform-ios": "^8.0.2", - "@react-native-community/cli-tools": "^8.0.0", - "chalk": "^4.1.2", - "command-exists": "^1.2.8", - "envinfo": "^7.7.2", - "execa": "^1.0.0", - "hermes-profile-transformer": "^0.0.6", - "ip": "^1.1.5", - "node-stream-zip": "^1.9.1", - "ora": "^5.4.1", - "prompts": "^2.4.0", - "semver": "^6.3.0", - "strip-ansi": "^5.2.0", - "sudo-prompt": "^9.0.0", - "wcwidth": "^1.0.1" - }, - "dependencies": { - "cross-spawn": { - "version": "6.0.5", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", - "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", - "dev": true, - "peer": true, - "requires": { - "nice-try": "^1.0.4", - "path-key": "^2.0.1", - "semver": "^5.5.0", - "shebang-command": "^1.2.0", - "which": "^1.2.9" - }, - "dependencies": { - "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true, - "peer": true - } - } - }, - "execa": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", - "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", - "dev": true, - "peer": true, - "requires": { - "cross-spawn": "^6.0.0", - "get-stream": "^4.0.0", - "is-stream": "^1.1.0", - "npm-run-path": "^2.0.0", - "p-finally": "^1.0.0", - "signal-exit": "^3.0.0", - "strip-eof": "^1.0.0" - } - }, - "get-stream": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", - "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", - "dev": true, - "peer": true, - "requires": { - "pump": "^3.0.0" - } - }, - "is-stream": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", - "integrity": "sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==", - "dev": true, - "peer": true - }, - "npm-run-path": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", - "integrity": "sha512-lJxZYlT4DW/bRUtFh1MQIWqmLwQfAxnqWG4HhEdjMlkrJYnJn0Jrr2u3mgxqaWsdiBc76TYkTG/mhrnYTuzfHw==", - "dev": true, - "peer": true, - "requires": { - "path-key": "^2.0.0" - } - }, - "path-key": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", - "integrity": "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==", - "dev": true, - "peer": true - }, - "shebang-command": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", - "integrity": "sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==", - "dev": true, - "peer": true, - "requires": { - "shebang-regex": "^1.0.0" - } - }, - "shebang-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", - "integrity": "sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==", - "dev": true, - "peer": true - }, - "which": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", - "dev": true, - "peer": true, - "requires": { - "isexe": "^2.0.0" - } - } - } - }, - "@react-native-community/cli-hermes": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/@react-native-community/cli-hermes/-/cli-hermes-8.0.2.tgz", - "integrity": "sha512-RZ9uHTf3UFtGTYxq88uENJEdaDB8ab+YPBDn+Li1u78IKwNeL04F0A1A3ab3hYUkG4PEPnL2rkYSlzzNFLOSPQ==", - "dev": true, - "peer": true, - "requires": { - "@react-native-community/cli-platform-android": "^8.0.2", - "@react-native-community/cli-tools": "^8.0.0", - "chalk": "^4.1.2", - "hermes-profile-transformer": "^0.0.6", - "ip": "^1.1.5" - } - }, - "@react-native-community/cli-platform-android": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/@react-native-community/cli-platform-android/-/cli-platform-android-8.0.2.tgz", - "integrity": "sha512-pAEkt+GULesr8FphTpaNYSmu+O1CPQ2zCXkAg4kRd0WXpq3BsVqomyDWd/eMXTkY/yYQMGl6KilU2p9r/hnfhA==", - "dev": true, - "peer": true, - "requires": { - "@react-native-community/cli-tools": "^8.0.0", - "chalk": "^4.1.2", - "execa": "^1.0.0", - "fs-extra": "^8.1.0", - "glob": "^7.1.3", - "jetifier": "^1.6.2", - "lodash": "^4.17.15", - "logkitty": "^0.7.1", - "slash": "^3.0.0" - }, - "dependencies": { - "cross-spawn": { - "version": "6.0.5", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", - "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", - "dev": true, - "peer": true, - "requires": { - "nice-try": "^1.0.4", - "path-key": "^2.0.1", - "semver": "^5.5.0", - "shebang-command": "^1.2.0", - "which": "^1.2.9" - } - }, - "execa": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", - "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", - "dev": true, - "peer": true, - "requires": { - "cross-spawn": "^6.0.0", - "get-stream": "^4.0.0", - "is-stream": "^1.1.0", - "npm-run-path": "^2.0.0", - "p-finally": "^1.0.0", - "signal-exit": "^3.0.0", - "strip-eof": "^1.0.0" - } - }, - "fs-extra": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", - "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", - "dev": true, - "peer": true, - "requires": { - "graceful-fs": "^4.2.0", - "jsonfile": "^4.0.0", - "universalify": "^0.1.0" - } - }, - "get-stream": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", - "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", - "dev": true, - "peer": true, - "requires": { - "pump": "^3.0.0" - } - }, - "is-stream": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", - "integrity": "sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==", - "dev": true, - "peer": true - }, - "jsonfile": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", - "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", - "dev": true, - "peer": true, - "requires": { - "graceful-fs": "^4.1.6" - } - }, - "npm-run-path": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", - "integrity": "sha512-lJxZYlT4DW/bRUtFh1MQIWqmLwQfAxnqWG4HhEdjMlkrJYnJn0Jrr2u3mgxqaWsdiBc76TYkTG/mhrnYTuzfHw==", - "dev": true, - "peer": true, - "requires": { - "path-key": "^2.0.0" - } - }, - "path-key": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", - "integrity": "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==", - "dev": true, - "peer": true - }, - "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true, - "peer": true - }, - "shebang-command": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", - "integrity": "sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==", - "dev": true, - "peer": true, - "requires": { - "shebang-regex": "^1.0.0" - } - }, - "shebang-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", - "integrity": "sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==", - "dev": true, - "peer": true - }, - "universalify": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", - "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", - "dev": true, - "peer": true - }, - "which": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", - "dev": true, - "peer": true, - "requires": { - "isexe": "^2.0.0" - } - } - } - }, - "@react-native-community/cli-platform-ios": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/@react-native-community/cli-platform-ios/-/cli-platform-ios-8.0.2.tgz", - "integrity": "sha512-LxWzj6jIZr5Ot893TKFbt0/T3WkVe6pbc/FSTo+TDQq1FQr/Urv16Uqn0AcL4IX2O1g3Qd13d0vtR/Cdpn3VNw==", - "dev": true, - "peer": true, - "requires": { - "@react-native-community/cli-tools": "^8.0.0", - "chalk": "^4.1.2", - "execa": "^1.0.0", - "glob": "^7.1.3", - "js-yaml": "^3.13.1", - "lodash": "^4.17.15", - "ora": "^5.4.1", - "plist": "^3.0.2" - }, - "dependencies": { - "argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "dev": true, - "peer": true, - "requires": { - "sprintf-js": "~1.0.2" - } - }, - "cross-spawn": { - "version": "6.0.5", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", - "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", - "dev": true, - "peer": true, - "requires": { - "nice-try": "^1.0.4", - "path-key": "^2.0.1", - "semver": "^5.5.0", - "shebang-command": "^1.2.0", - "which": "^1.2.9" - } - }, - "execa": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", - "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", - "dev": true, - "peer": true, - "requires": { - "cross-spawn": "^6.0.0", - "get-stream": "^4.0.0", - "is-stream": "^1.1.0", - "npm-run-path": "^2.0.0", - "p-finally": "^1.0.0", - "signal-exit": "^3.0.0", - "strip-eof": "^1.0.0" - } - }, - "get-stream": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", - "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", - "dev": true, - "peer": true, - "requires": { - "pump": "^3.0.0" - } - }, - "is-stream": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", - "integrity": "sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==", - "dev": true, - "peer": true - }, - "js-yaml": { - "version": "3.14.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", - "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", - "dev": true, - "peer": true, - "requires": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" - } - }, - "npm-run-path": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", - "integrity": "sha512-lJxZYlT4DW/bRUtFh1MQIWqmLwQfAxnqWG4HhEdjMlkrJYnJn0Jrr2u3mgxqaWsdiBc76TYkTG/mhrnYTuzfHw==", - "dev": true, - "peer": true, - "requires": { - "path-key": "^2.0.0" - } - }, - "path-key": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", - "integrity": "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==", - "dev": true, - "peer": true - }, - "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true, - "peer": true - }, - "shebang-command": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", - "integrity": "sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==", - "dev": true, - "peer": true, - "requires": { - "shebang-regex": "^1.0.0" - } - }, - "shebang-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", - "integrity": "sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==", - "dev": true, - "peer": true - }, - "which": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", - "dev": true, - "peer": true, - "requires": { - "isexe": "^2.0.0" - } - } - } - }, - "@react-native-community/cli-plugin-metro": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/@react-native-community/cli-plugin-metro/-/cli-plugin-metro-8.0.0.tgz", - "integrity": "sha512-eIowV2ZRbzIWL3RIKVUUSahntXTuAeKzBSsFuhffLZphsV+UdKdtg5ATR9zbq7nsKap4ZseO5DkVqZngUkC7iQ==", - "dev": true, - "peer": true, - "requires": { - "@react-native-community/cli-server-api": "^8.0.0", - "@react-native-community/cli-tools": "^8.0.0", - "chalk": "^4.1.2", - "metro": "^0.70.1", - "metro-config": "^0.70.1", - "metro-core": "^0.70.1", - "metro-react-native-babel-transformer": "^0.70.1", - "metro-resolver": "^0.70.1", - "metro-runtime": "^0.70.1", - "readline": "^1.3.0" - } - }, - "@react-native-community/cli-server-api": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/@react-native-community/cli-server-api/-/cli-server-api-8.0.0.tgz", - "integrity": "sha512-TxUs3sMl9clt7sdv30XETc6VRzyaEli2vDrk3TB5W5o5nSd1PmQdP4ccdGLO/nDRXwOy72QmmXlYWMg1XGU0Gg==", - "dev": true, - "peer": true, - "requires": { - "@react-native-community/cli-debugger-ui": "^8.0.0", - "@react-native-community/cli-tools": "^8.0.0", - "compression": "^1.7.1", - "connect": "^3.6.5", - "errorhandler": "^1.5.0", - "nocache": "^3.0.1", - "pretty-format": "^26.6.2", - "serve-static": "^1.13.1", - "ws": "^7.5.1" - }, - "dependencies": { - "ws": { - "version": "7.5.8", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.8.tgz", - "integrity": "sha512-ri1Id1WinAX5Jqn9HejiGb8crfRio0Qgu8+MtL36rlTA6RLsMdWt1Az/19A2Qij6uSHUMphEFaTKa4WG+UNHNw==", - "dev": true, - "peer": true, - "requires": {} - } - } - }, - "@react-native-community/cli-tools": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/@react-native-community/cli-tools/-/cli-tools-8.0.0.tgz", - "integrity": "sha512-jA4y8CebrRZaOJFjc5zMOnls4KfHkBl2FUtBZV2vcWuedQHa6JVwo+KO88ta3Ysby3uY0+mrZagZfXk7c0mrBw==", - "dev": true, - "peer": true, - "requires": { - "appdirsjs": "^1.2.4", - "chalk": "^4.1.2", - "lodash": "^4.17.15", - "mime": "^2.4.1", - "node-fetch": "^2.6.0", - "open": "^6.2.0", - "ora": "^5.4.1", - "semver": "^6.3.0", - "shell-quote": "^1.7.3" - }, - "dependencies": { - "mime": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", - "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", - "dev": true, - "peer": true - } - } - }, - "@react-native-community/cli-types": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/@react-native-community/cli-types/-/cli-types-8.0.0.tgz", - "integrity": "sha512-1lZS1PEvMlFaN3Se1ksyoFWzMjk+YfKi490GgsqKJln9gvFm8tqVPdnXttI5Uf2DQf3BMse8Bk8dNH4oV6Ewow==", - "dev": true, - "peer": true, - "requires": { - "joi": "^17.2.1" - } - }, - "@react-native-community/netinfo": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/@react-native-community/netinfo/-/netinfo-4.7.0.tgz", - "integrity": "sha512-a/sDB+AsLEUNmhAUlAaTYeXKyQdFGBUfatqKkX5jluBo2CB3OAuTHfm7rSjcaLB9EmG5iSq3fOTpync2E7EYTA==", - "dev": true, - "requires": {} - }, - "@react-native/assets": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@react-native/assets/-/assets-1.0.0.tgz", - "integrity": "sha512-KrwSpS1tKI70wuKl68DwJZYEvXktDHdZMG0k2AXD/rJVSlB23/X2CB2cutVR0HwNMJIal9HOUOBB2rVfa6UGtQ==", - "dev": true, - "peer": true - }, - "@react-native/normalize-color": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@react-native/normalize-color/-/normalize-color-2.0.0.tgz", - "integrity": "sha512-Wip/xsc5lw8vsBlmY2MO/gFLp3MvuZ2baBZjDeTjjndMgM0h5sxz7AZR62RDPGgstp8Np7JzjvVqVT7tpFZqsw==", - "dev": true, - "peer": true - }, - "@react-native/polyfills": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@react-native/polyfills/-/polyfills-2.0.0.tgz", - "integrity": "sha512-K0aGNn1TjalKj+65D7ycc1//H9roAQ51GJVk5ZJQFb2teECGmzd86bYDC0aYdbRf7gtovescq4Zt6FR0tgXiHQ==", - "dev": true, - "peer": true - }, "@servie/events": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@servie/events/-/events-1.0.0.tgz", "integrity": "sha512-sBSO19KzdrJCM3gdx6eIxV8M9Gxfgg6iDQmH5TIAGaUu+X9VDdsINXJOnoiZ1Kx3TrHdH4bt5UVglkjsEGBcvw==", "dev": true }, - "@sideway/address": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.4.tgz", - "integrity": "sha512-7vwq+rOHVWjyXxVlR76Agnvhy8I9rpzjosTESvmhNeXOXdZZB15Fl+TI9x1SiHZH5Jv2wTGduSxFDIaq0m3DUw==", - "dev": true, - "peer": true, - "requires": { - "@hapi/hoek": "^9.0.0" - } - }, - "@sideway/formula": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.0.tgz", - "integrity": "sha512-vHe7wZ4NOXVfkoRb8T5otiENVlT7a3IAiw7H5M2+GO+9CDgcVUUsX1zalAztCmwyOr2RUTGJdgB+ZvSVqmdHmg==", - "dev": true, - "peer": true - }, - "@sideway/pinpoint": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@sideway/pinpoint/-/pinpoint-2.0.0.tgz", - "integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==", - "dev": true, - "peer": true - }, "@testing-library/cypress": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/@testing-library/cypress/-/cypress-9.0.0.tgz", - "integrity": "sha512-c1XiCGeHGGTWn0LAU12sFUfoX3qfId5gcSE2yHode+vsyHDWraxDPALjVnHd4/Fa3j4KBcc5k++Ccy6A9qnkMA==", + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@testing-library/cypress/-/cypress-10.0.1.tgz", + "integrity": "sha512-e8uswjTZIBhaIXjzEcrQQ8nHRWHgZH7XBxKuIWxZ/T7FxfWhCR48nFhUX5nfPizjVOKSThEfOSv67jquc1ASkw==", "dev": true, "requires": { "@babel/runtime": "^7.14.6", - "@testing-library/dom": "^8.1.0" + "@testing-library/dom": "^9.0.0" } }, "@testing-library/dom": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-8.16.0.tgz", - "integrity": "sha512-uxF4zmnLHHDlmW4l+0WDjcgLVwCvH+OVLpD8Dfp+Bjfz85prwxWGbwXgJdLtkgjD0qfOzkJF9SmA6YZPsMYX4w==", + "version": "9.3.3", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-9.3.3.tgz", + "integrity": "sha512-fB0R+fa3AUqbLHWyxXa2kGVtf1Fe1ZZFr0Zp6AIbIAzXb2mKbEXl+PCQNUOaq5lbTab5tfctfXRNsWXxa2f7Aw==", "dev": true, "requires": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", - "@types/aria-query": "^4.2.0", - "aria-query": "^5.0.0", + "@types/aria-query": "^5.0.1", + "aria-query": "5.1.3", "chalk": "^4.1.0", "dom-accessibility-api": "^0.5.9", - "lz-string": "^1.4.4", + "lz-string": "^1.5.0", "pretty-format": "^27.0.2" }, "dependencies": { + "ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true + }, "pretty-format": { "version": "27.5.1", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", @@ -20033,34 +10260,26 @@ "ansi-regex": "^5.0.1", "ansi-styles": "^5.0.0", "react-is": "^17.0.1" - }, - "dependencies": { - "ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true - } } } } }, "@types/aria-query": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-4.2.2.tgz", - "integrity": "sha512-HnYpAE1Y6kRyKM/XkEuiRQhTHvkzMBurTHnpFLYLBGPIylZNPs9jJcuOOYWxPLJCSEtmZT0Y8rHDokKN7rRTig==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.1.tgz", + "integrity": "sha512-XTIieEY+gvJ39ChLcB4If5zHtPxt3Syj5rgZR+e1ctpmK8NjPf0zFqsz4JpLJT0xla9GFDKjy8Cpu331nrmE1Q==", "dev": true }, "@types/async": { - "version": "3.2.20", - "resolved": "https://registry.npmjs.org/@types/async/-/async-3.2.20.tgz", - "integrity": "sha512-6jSBQQugzyX1aWto0CbvOnmxrU9tMoXfA9gc4IrLEtvr3dTwSg5GLGoWiZnGLI6UG/kqpB3JOQKQrqnhUWGKQA==", + "version": "3.2.21", + "resolved": "https://registry.npmjs.org/@types/async/-/async-3.2.21.tgz", + "integrity": "sha512-msYM0OYxzkwpiDJZZDo7XsYuFIpiAOh+o3hygudSpSzeW+R0DAEKZvdQriHYT95m9CvscLDg5s0Mxlrlhoy2qw==", "dev": true }, "@types/authenticator": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@types/authenticator/-/authenticator-1.1.1.tgz", - "integrity": "sha512-yEIqr179ISDa7XM5g/+aKGi+lXSVkheR8pWtVhWAM1G+v1aLari92SkTUIZPCI1Lpk+PhXGWdCtehm99IqoOFg==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@types/authenticator/-/authenticator-1.1.2.tgz", + "integrity": "sha512-0GksFKLfPTubVYNpwQbQ/TTumK6YFA+yxh4KLSe7QrnY/0bI27P5YJwKQDLF4NPOtJm+ywKg+ptwP8Za4GLtxQ==", "dev": true }, "@types/body-parser": { @@ -20112,9 +10331,9 @@ "peer": true }, "@types/express": { - "version": "4.17.17", - "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.17.tgz", - "integrity": "sha512-Q4FmmuLGBG58btUnfS1c1r/NQdlp3DMfGDGig8WhfpA2YRUtEkxAjkZb0yvplJGYdF1fsQ81iMDcH24sSCNC/Q==", + "version": "4.17.18", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.18.tgz", + "integrity": "sha512-Sxv8BSLLgsBYmcnGdGjjEjqET2U+AKAdCRODmMiq02FgjwuV75Ut85DRpvFjyw/Mk0vgUOliGRU0UUmuuZHByQ==", "dev": true, "requires": { "@types/body-parser": "*", @@ -20136,9 +10355,9 @@ } }, "@types/fs-extra": { - "version": "11.0.1", - "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-11.0.1.tgz", - "integrity": "sha512-MxObHvNl4A69ofaTRU8DFqvgzzv8s9yRtaPPm5gud9HDNvpB3GPQFvNuTWAI59B9huVGV5jXYJwbCsmBsOGYWA==", + "version": "11.0.2", + "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-11.0.2.tgz", + "integrity": "sha512-c0hrgAOVYr21EX8J0jBMXGLMgJqVf/v6yxi0dLaJboW9aQPh16Id+z6w2Tx1hm+piJOLv8xPfVKZCLfjPw/IMQ==", "dev": true, "requires": { "@types/jsonfile": "*", @@ -20155,47 +10374,10 @@ "@types/node": "*" } }, - "@types/graceful-fs": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.5.tgz", - "integrity": "sha512-anKkLmZZ+xm4p8JWBf4hElkM4XR+EZeA2M9BAkkTldmcyDY4mbdIJnRghDJH3Ov5ooY7/UAoENtmdMSkaAd7Cw==", - "dev": true, - "peer": true, - "requires": { - "@types/node": "*" - } - }, - "@types/istanbul-lib-coverage": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz", - "integrity": "sha512-z/QT1XN4K4KYuslS23k62yDIDLwLFkzxOuMplDtObz0+y7VqJCaO2o+SPwHCvLFZh7xazvvoor2tA/hPz9ee7g==", - "dev": true, - "peer": true - }, - "@types/istanbul-lib-report": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz", - "integrity": "sha512-plGgXAPfVKFoYfa9NpYDAkseG+g6Jr294RqeqcqDixSbU34MZVJRi/P+7Y8GDpzkEwLaGZZOpKIEmeVZNtKsrg==", - "dev": true, - "peer": true, - "requires": { - "@types/istanbul-lib-coverage": "*" - } - }, - "@types/istanbul-reports": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.1.tgz", - "integrity": "sha512-c3mAZEuK0lvBp8tmuL74XRKn1+y2dcwOUpH7x4WrF6gk1GIgiluDRgMYQtw2OFcBvAJWlt6ASU3tSqxp0Uu0Aw==", - "dev": true, - "peer": true, - "requires": { - "@types/istanbul-lib-report": "*" - } - }, "@types/json-schema": { - "version": "7.0.11", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz", - "integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==", + "version": "7.0.13", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.13.tgz", + "integrity": "sha512-RbSSoHliUbnXj3ny0CNFOoxrIDV6SUGyStHsvDqosw6CkdPV8TtWGlfecuK4ToyMEAql6pzNxgCFKanovUzlgQ==", "dev": true }, "@types/json5": { @@ -20214,9 +10396,9 @@ } }, "@types/lodash": { - "version": "4.14.194", - "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.194.tgz", - "integrity": "sha512-r22s9tAS7imvBt2lyHC9B8AGwWnXaYb1tY09oyLkXDs4vArpYJzw09nj8MLx5VfciBPGIb+ZwG0ssYnEPJxn/g==", + "version": "4.14.199", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.199.tgz", + "integrity": "sha512-Vrjz5N5Ia4SEzWWgIVwnHNEnb1UE1XMkvY5DGXrAeOGE9imk0hgTHh5GyDjLDJi9OTCn9oo9dXH1uToK1VRfrg==", "dev": true }, "@types/lodash.intersection": { @@ -20253,9 +10435,9 @@ "dev": true }, "@types/mime-types": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@types/mime-types/-/mime-types-2.1.1.tgz", - "integrity": "sha512-vXOTGVSLR2jMw440moWTC7H19iUyLtP3Z1YTj7cSsubOICinjMxFeb/V57v9QdyyPGbbWolUFSSmSiRSn94tFw==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@types/mime-types/-/mime-types-2.1.2.tgz", + "integrity": "sha512-q9QGHMGCiBJCHEvd4ZLdasdqXv570agPsUW0CeIm/B8DzhxsYMerD0l3IlI+EQ1A2RWHY2mmM9x1YIuuWxisCg==", "dev": true }, "@types/minimatch": { @@ -20271,24 +10453,24 @@ "dev": true }, "@types/mochawesome": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/@types/mochawesome/-/mochawesome-6.2.1.tgz", - "integrity": "sha512-AhQdBkT/CBdx3sI9ATeljqa5uJ3dGNKEJnsgzw9IkPeg9d9Lzxsz1eKFnenxq1qQojIW0XkIsCgdheRRXP2SQA==", + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/@types/mochawesome/-/mochawesome-6.2.2.tgz", + "integrity": "sha512-UqE7Mbw6Z2Nk3gXjbAM43p+Z3LpfH4624JtihzsR17sRlZo1UFBT0/zznE82wXvnI4gB/xWz0BxfwQpP0qB3Uw==", "dev": true, "requires": { "@types/mocha": "*" } }, "@types/node": { - "version": "14.17.5", - "resolved": "https://registry.npmjs.org/@types/node/-/node-14.17.5.tgz", - "integrity": "sha512-bjqH2cX/O33jXT/UmReo2pM7DIJREPMnarixbQ57DOOzzFaI6D2+IcwaJQaJpv0M1E9TIhPCYVxrkcityLjlqA==", + "version": "18.17.18", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.17.18.tgz", + "integrity": "sha512-/4QOuy3ZpV7Ya1GTRz5CYSz3DgkKpyUptXuQ5PPce7uuyJAOR7r9FhkmxJfvcNUXyklbC63a+YvB3jxy7s9ngw==", "dev": true }, "@types/pdf-parse": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@types/pdf-parse/-/pdf-parse-1.1.1.tgz", - "integrity": "sha512-lDBKAslCwvfK2uvS1Uk+UCpGvw+JRy5vnBFANPKFSY92n/iEnunXi0KVBjPJXhsM4jtdcPnS7tuZ0zjA9x6piQ==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@types/pdf-parse/-/pdf-parse-1.1.2.tgz", + "integrity": "sha512-4ftxqGUcYYjOmh6aTHvFZk9Y7N4j1+UCqBv1hRz+21zXnr08x9ZZeYovJUjyK7r7TthuLX+UND9Q42KFpbvF0g==", "dev": true }, "@types/qs": { @@ -20304,18 +10486,18 @@ "dev": true }, "@types/recursive-readdir": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/@types/recursive-readdir/-/recursive-readdir-2.2.1.tgz", - "integrity": "sha512-Xd+Ptc4/F2ueInqy5yK2FI5FxtwwbX2+VZpcg+9oYsFJVen8qQKGapCr+Bi5wQtHU1cTXT8s+07lo/nKPgu8Gg==", + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/@types/recursive-readdir/-/recursive-readdir-2.2.2.tgz", + "integrity": "sha512-pYLuc3EaFA3SwYfZRC6cSV9TfL+0tLi9Otu2djGavsNhmrEna1yG1EkFOZW7SKJr/nIuDNk4mM6kMMpNLntIhw==", "dev": true, "requires": { "@types/node": "*" } }, "@types/semver": { - "version": "7.3.13", - "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.3.13.tgz", - "integrity": "sha512-21cFJr9z3g5dW8B0CVI9g2O9beqaThGQ6ZFBqHfwhzLDKUxaqTIy3vnfah/UPkfOiF2pLq+tGz+W8RyCskuslw==", + "version": "7.5.3", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.3.tgz", + "integrity": "sha512-OxepLK9EuNEIPxWNME+C6WwbRAOOI2o2BaQEGzz5Lu2e4Z5eDnEo+/aVEDMIXywoJitJ7xWd641wrGLZdtwRyw==", "dev": true }, "@types/send": { @@ -20339,9 +10521,9 @@ } }, "@types/shelljs": { - "version": "0.8.12", - "resolved": "https://registry.npmjs.org/@types/shelljs/-/shelljs-0.8.12.tgz", - "integrity": "sha512-ZA8U81/gldY+rR5zl/7HSHrG2KDfEb3lzG6uCUDhW1DTQE9yC/VBQ45fXnXq8f3CgInfhZmjtdu/WOUlrXRQUg==", + "version": "0.8.13", + "resolved": "https://registry.npmjs.org/@types/shelljs/-/shelljs-0.8.13.tgz", + "integrity": "sha512-++uMLOQSLlse1kCfEOwhgmHuaABZwinkylmUKCpvcEGZUov3TtM+gJZloSkW/W+9pEAEg/VBOwiSR05oqJsa5A==", "dev": true, "requires": { "@types/glob": "~7.2.0", @@ -20367,28 +10549,11 @@ "dev": true }, "@types/uuid": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.1.tgz", - "integrity": "sha512-rFT3ak0/2trgvp4yYZo5iKFEPsET7vKydKF+VRCxlQ9bpheehyAJH89dAkaLEq/j/RZXJIqcgsmPJKUP1Z28HA==", + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.4.tgz", + "integrity": "sha512-zAuJWQflfx6dYJM62vna+Sn5aeSWhh3OB+wfUEACNcqUSc0AGc5JKl+ycL1vrH7frGTXhJchYjE1Hak8L819dA==", "dev": true }, - "@types/yargs": { - "version": "16.0.4", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-16.0.4.tgz", - "integrity": "sha512-T8Yc9wt/5LbJyCaLiHPReJa0kApcIgJ7Bn735GjItUfh08Z1pJvu8QZqb9s+mMvKV6WUQRV7K2R46YbjMXTTJw==", - "dev": true, - "peer": true, - "requires": { - "@types/yargs-parser": "*" - } - }, - "@types/yargs-parser": { - "version": "21.0.0", - "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.0.tgz", - "integrity": "sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA==", - "dev": true, - "peer": true - }, "@types/yauzl": { "version": "2.9.2", "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.9.2.tgz", @@ -20400,27 +10565,28 @@ } }, "@typescript-eslint/eslint-plugin": { - "version": "5.59.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.59.2.tgz", - "integrity": "sha512-yVrXupeHjRxLDcPKL10sGQ/QlVrA8J5IYOEWVqk0lJaSZP7X5DfnP7Ns3cc74/blmbipQ1htFNVGsHX6wsYm0A==", + "version": "6.7.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.7.4.tgz", + "integrity": "sha512-DAbgDXwtX+pDkAHwiGhqP3zWUGpW49B7eqmgpPtg+BKJXwdct79ut9+ifqOFPJGClGKSHXn2PTBatCnldJRUoA==", "dev": true, "requires": { - "@eslint-community/regexpp": "^4.4.0", - "@typescript-eslint/scope-manager": "5.59.2", - "@typescript-eslint/type-utils": "5.59.2", - "@typescript-eslint/utils": "5.59.2", + "@eslint-community/regexpp": "^4.5.1", + "@typescript-eslint/scope-manager": "6.7.4", + "@typescript-eslint/type-utils": "6.7.4", + "@typescript-eslint/utils": "6.7.4", + "@typescript-eslint/visitor-keys": "6.7.4", "debug": "^4.3.4", - "grapheme-splitter": "^1.0.4", - "ignore": "^5.2.0", - "natural-compare-lite": "^1.4.0", - "semver": "^7.3.7", - "tsutils": "^3.21.0" + "graphemer": "^1.4.0", + "ignore": "^5.2.4", + "natural-compare": "^1.4.0", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" }, "dependencies": { "semver": { - "version": "7.3.8", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", - "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", "dev": true, "requires": { "lru-cache": "^6.0.0" @@ -20429,64 +10595,65 @@ } }, "@typescript-eslint/parser": { - "version": "5.59.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.59.2.tgz", - "integrity": "sha512-uq0sKyw6ao1iFOZZGk9F8Nro/8+gfB5ezl1cA06SrqbgJAt0SRoFhb9pXaHvkrxUpZaoLxt8KlovHNk8Gp6/HQ==", + "version": "6.7.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.7.4.tgz", + "integrity": "sha512-I5zVZFY+cw4IMZUeNCU7Sh2PO5O57F7Lr0uyhgCJmhN/BuTlnc55KxPonR4+EM3GBdfiCyGZye6DgMjtubQkmA==", "dev": true, "requires": { - "@typescript-eslint/scope-manager": "5.59.2", - "@typescript-eslint/types": "5.59.2", - "@typescript-eslint/typescript-estree": "5.59.2", + "@typescript-eslint/scope-manager": "6.7.4", + "@typescript-eslint/types": "6.7.4", + "@typescript-eslint/typescript-estree": "6.7.4", + "@typescript-eslint/visitor-keys": "6.7.4", "debug": "^4.3.4" } }, "@typescript-eslint/scope-manager": { - "version": "5.59.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.59.2.tgz", - "integrity": "sha512-dB1v7ROySwQWKqQ8rEWcdbTsFjh2G0vn8KUyvTXdPoyzSL6lLGkiXEV5CvpJsEe9xIdKV+8Zqb7wif2issoOFA==", + "version": "6.7.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.7.4.tgz", + "integrity": "sha512-SdGqSLUPTXAXi7c3Ob7peAGVnmMoGzZ361VswK2Mqf8UOYcODiYvs8rs5ILqEdfvX1lE7wEZbLyELCW+Yrql1A==", "dev": true, "requires": { - "@typescript-eslint/types": "5.59.2", - "@typescript-eslint/visitor-keys": "5.59.2" + "@typescript-eslint/types": "6.7.4", + "@typescript-eslint/visitor-keys": "6.7.4" } }, "@typescript-eslint/type-utils": { - "version": "5.59.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.59.2.tgz", - "integrity": "sha512-b1LS2phBOsEy/T381bxkkywfQXkV1dWda/z0PhnIy3bC5+rQWQDS7fk9CSpcXBccPY27Z6vBEuaPBCKCgYezyQ==", + "version": "6.7.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.7.4.tgz", + "integrity": "sha512-n+g3zi1QzpcAdHFP9KQF+rEFxMb2KxtnJGID3teA/nxKHOVi3ylKovaqEzGBbVY2pBttU6z85gp0D00ufLzViQ==", "dev": true, "requires": { - "@typescript-eslint/typescript-estree": "5.59.2", - "@typescript-eslint/utils": "5.59.2", + "@typescript-eslint/typescript-estree": "6.7.4", + "@typescript-eslint/utils": "6.7.4", "debug": "^4.3.4", - "tsutils": "^3.21.0" + "ts-api-utils": "^1.0.1" } }, "@typescript-eslint/types": { - "version": "5.59.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.59.2.tgz", - "integrity": "sha512-LbJ/HqoVs2XTGq5shkiKaNTuVv5tTejdHgfdjqRUGdYhjW1crm/M7og2jhVskMt8/4wS3T1+PfFvL1K3wqYj4w==", + "version": "6.7.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.7.4.tgz", + "integrity": "sha512-o9XWK2FLW6eSS/0r/tgjAGsYasLAnOWg7hvZ/dGYSSNjCh+49k5ocPN8OmG5aZcSJ8pclSOyVKP2x03Sj+RrCA==", "dev": true }, "@typescript-eslint/typescript-estree": { - "version": "5.59.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.59.2.tgz", - "integrity": "sha512-+j4SmbwVmZsQ9jEyBMgpuBD0rKwi9RxRpjX71Brr73RsYnEr3Lt5QZ624Bxphp8HUkSKfqGnPJp1kA5nl0Sh7Q==", + "version": "6.7.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.7.4.tgz", + "integrity": "sha512-ty8b5qHKatlNYd9vmpHooQz3Vki3gG+3PchmtsA4TgrZBKWHNjWfkQid7K7xQogBqqc7/BhGazxMD5vr6Ha+iQ==", "dev": true, "requires": { - "@typescript-eslint/types": "5.59.2", - "@typescript-eslint/visitor-keys": "5.59.2", + "@typescript-eslint/types": "6.7.4", + "@typescript-eslint/visitor-keys": "6.7.4", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", - "semver": "^7.3.7", - "tsutils": "^3.21.0" + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" }, "dependencies": { "semver": { - "version": "7.5.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.0.tgz", - "integrity": "sha512-+XC0AD/R7Q2mPSRuy2Id0+CGTZ98+8f+KvwirxOKIEyid+XSx6HbC63p+O4IndTHuX5Z+JxQ0TghCkO5Cg/2HA==", + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", "dev": true, "requires": { "lru-cache": "^6.0.0" @@ -20495,25 +10662,24 @@ } }, "@typescript-eslint/utils": { - "version": "5.59.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.59.2.tgz", - "integrity": "sha512-kSuF6/77TZzyGPhGO4uVp+f0SBoYxCDf+lW3GKhtKru/L8k/Hd7NFQxyWUeY7Z/KGB2C6Fe3yf2vVi4V9TsCSQ==", + "version": "6.7.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.7.4.tgz", + "integrity": "sha512-PRQAs+HUn85Qdk+khAxsVV+oULy3VkbH3hQ8hxLRJXWBEd7iI+GbQxH5SEUSH7kbEoTp6oT1bOwyga24ELALTA==", "dev": true, "requires": { - "@eslint-community/eslint-utils": "^4.2.0", - "@types/json-schema": "^7.0.9", - "@types/semver": "^7.3.12", - "@typescript-eslint/scope-manager": "5.59.2", - "@typescript-eslint/types": "5.59.2", - "@typescript-eslint/typescript-estree": "5.59.2", - "eslint-scope": "^5.1.1", - "semver": "^7.3.7" + "@eslint-community/eslint-utils": "^4.4.0", + "@types/json-schema": "^7.0.12", + "@types/semver": "^7.5.0", + "@typescript-eslint/scope-manager": "6.7.4", + "@typescript-eslint/types": "6.7.4", + "@typescript-eslint/typescript-estree": "6.7.4", + "semver": "^7.5.4" }, "dependencies": { "semver": { - "version": "7.5.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.0.tgz", - "integrity": "sha512-+XC0AD/R7Q2mPSRuy2Id0+CGTZ98+8f+KvwirxOKIEyid+XSx6HbC63p+O4IndTHuX5Z+JxQ0TghCkO5Cg/2HA==", + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", "dev": true, "requires": { "lru-cache": "^6.0.0" @@ -20522,13 +10688,13 @@ } }, "@typescript-eslint/visitor-keys": { - "version": "5.59.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.59.2.tgz", - "integrity": "sha512-EEpsO8m3RASrKAHI9jpavNv9NlEUebV4qmF1OWxSTtKSFBpC1NCmWazDQHFivRf0O1DV11BA645yrLEVQ0/Lig==", + "version": "6.7.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.7.4.tgz", + "integrity": "sha512-pOW37DUhlTZbvph50x5zZCkFn3xzwkGtNoJHzIM3svpiSkJzwOYr/kVBaXmf+RAQiUDs1AHEZVNPg6UJCJpwRA==", "dev": true, "requires": { - "@typescript-eslint/types": "5.59.2", - "eslint-visitor-keys": "^3.3.0" + "@typescript-eslint/types": "6.7.4", + "eslint-visitor-keys": "^3.4.1" } }, "@webassemblyjs/ast": { @@ -20706,28 +10872,6 @@ "dev": true, "peer": true }, - "@yarnpkg/lockfile": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz", - "integrity": "sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ==" - }, - "abort-controller": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", - "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", - "dev": true, - "peer": true, - "requires": { - "event-target-shim": "^5.0.0" - } - }, - "absolute-path": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/absolute-path/-/absolute-path-0.0.0.tgz", - "integrity": "sha512-HQiug4c+/s3WOvEnDRxXVmNtSG5s2gJM9r19BTcqjp7BWcE48PB+Y2G6jE65kqI0LpsQeMZygt/b60Gi4KxGyA==", - "dev": true, - "peer": true - }, "accepts": { "version": "1.3.8", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", @@ -20739,9 +10883,9 @@ } }, "acorn": { - "version": "8.8.2", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz", - "integrity": "sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==", + "version": "8.10.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", + "integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==", "dev": true }, "acorn-jsx": { @@ -20791,13 +10935,6 @@ "platform": "1.3.3" } }, - "anser": { - "version": "1.4.10", - "resolved": "https://registry.npmjs.org/anser/-/anser-1.4.10.tgz", - "integrity": "sha512-hCv9AqTQ8ycjpSd3upOJd7vFwW1JaoYQ7tpham03GJ1ca8/65rqn0RpaWpItOAd6ylW9wAw6luXYPJIyPFVOww==", - "dev": true, - "peer": true - }, "ansi-colors": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", @@ -20813,73 +10950,6 @@ "type-fest": "^0.21.3" } }, - "ansi-fragments": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/ansi-fragments/-/ansi-fragments-0.2.1.tgz", - "integrity": "sha512-DykbNHxuXQwUDRv5ibc2b0x7uw7wmwOGLBUd5RmaQ5z8Lhx19vwvKV+FAsM5rEA6dEcHxX+/Ad5s9eF2k2bB+w==", - "dev": true, - "peer": true, - "requires": { - "colorette": "^1.0.7", - "slice-ansi": "^2.0.0", - "strip-ansi": "^5.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "peer": true, - "requires": { - "color-convert": "^1.9.0" - } - }, - "astral-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-1.0.0.tgz", - "integrity": "sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==", - "dev": true, - "peer": true - }, - "color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "peer": true, - "requires": { - "color-name": "1.1.3" - } - }, - "color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true, - "peer": true - }, - "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w==", - "dev": true, - "peer": true - }, - "slice-ansi": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-2.1.0.tgz", - "integrity": "sha512-Qu+VC3EwYLldKa1fCxuuvULvSJOKEgk9pi8dZeCVK7TqBfUNTH4sFkk4joj8afVSfAYgJoSOetjx9QWOJ5mYoQ==", - "dev": true, - "peer": true, - "requires": { - "ansi-styles": "^3.2.0", - "astral-regex": "^1.0.0", - "is-fullwidth-code-point": "^2.0.0" - } - } - } - }, "ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", @@ -20890,6 +10960,7 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, "requires": { "color-convert": "^2.0.1" } @@ -20904,13 +10975,6 @@ "picomatch": "^2.0.4" } }, - "appdirsjs": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/appdirsjs/-/appdirsjs-1.2.7.tgz", - "integrity": "sha512-Quji6+8kLBC3NnBeo14nPDq0+2jUs5s3/xEye+udFHumHhRk4M7aAMXp/PBJqkKYGuuyR9M/6Dq7d2AViiGmhw==", - "dev": true, - "peer": true - }, "arch": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/arch/-/arch-2.2.0.tgz", @@ -20932,27 +10996,6 @@ "deep-equal": "^2.0.5" } }, - "arr-diff": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", - "integrity": "sha512-YVIQ82gZPGBebQV/a8dar4AitzCQs0jjXwMPZllpXMaGjXPYVUawSxQrRsjhjupyVxEvbHgUmIhKVlND+j02kA==", - "dev": true, - "peer": true - }, - "arr-flatten": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz", - "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==", - "dev": true, - "peer": true - }, - "arr-union": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", - "integrity": "sha512-sKpyeERZ02v1FeCZT8lrfJq5u6goHCtpTAzPwJYe7c8SPFOboNjNg1vz2L4VTn9T4PQxEx13TbXLmYUcS6Ug7Q==", - "dev": true, - "peer": true - }, "array-buffer-byte-length": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.0.tgz", @@ -20963,12 +11006,6 @@ "is-array-buffer": "^3.0.1" } }, - "array-find": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/array-find/-/array-find-1.0.0.tgz", - "integrity": "sha512-kO/vVCacW9mnpn3WPWbTVlEnOabK2L7LWi2HViURtCM46y1zb6I8UMjx4LgbiqadTgHnLInUronwn3ampNTJtQ==", - "dev": true - }, "array-flatten": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", @@ -20994,12 +11031,30 @@ "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", "dev": true }, - "array-unique": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", - "integrity": "sha512-SleRWjh9JUud2wH1hPs9rZBZ33H6T9HOiL0uwGnGx9FpE6wKGyfWugmbkEOIs6qWrZhg0LWeLziLrEwQJhs5mQ==", + "array.prototype.find": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/array.prototype.find/-/array.prototype.find-2.2.2.tgz", + "integrity": "sha512-DRumkfW97iZGOfn+lIXbkVrXL04sfYKX+EfOodo8XboR5sxPDVvOjZTF/rysusa9lmhmSOeD6Vp6RKQP+eP4Tg==", "dev": true, - "peer": true + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "es-shim-unscopables": "^1.0.0" + } + }, + "array.prototype.findlastindex": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.3.tgz", + "integrity": "sha512-LzLoiOMAxvy+Gd3BAq3B7VeIgPdo+Q8hthvKtXybMvRV0jrXfJM/t8mw7nNlpEcVlVUnCnM2KSX4XU5HmpodOA==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "es-shim-unscopables": "^1.0.0", + "get-intrinsic": "^1.2.1" + } }, "array.prototype.flat": { "version": "1.3.1", @@ -21038,12 +11093,20 @@ "get-intrinsic": "^1.1.3" } }, - "asap": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", - "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", + "arraybuffer.prototype.slice": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.2.tgz", + "integrity": "sha512-yMBKppFur/fbHu9/6USUe03bZ4knMYiwFBcyiaXB8Go0qNehwX6inYPzK9U0NeQvGxKthcmHcaR8P5MStSRBAw==", "dev": true, - "peer": true + "requires": { + "array-buffer-byte-length": "^1.0.0", + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "get-intrinsic": "^1.2.1", + "is-array-buffer": "^3.0.2", + "is-shared-array-buffer": "^1.0.2" + } }, "asn1": { "version": "0.2.6", @@ -21066,23 +11129,6 @@ "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", "dev": true }, - "assign-symbols": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", - "integrity": "sha512-Q+JC7Whu8HhmTdBph/Tq59IoRtoy6KAm5zzPv00WdujX82lbAL8K7WVjne7vdCsAmbF4AYaDOPyO3k0kl8qIrw==", - "dev": true, - "peer": true - }, - "ast-types": { - "version": "0.14.2", - "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.14.2.tgz", - "integrity": "sha512-O0yuUDnZeQDL+ncNGlJ78BiO4jnYI3bvMsD5prT0/nsgijG/LpNBIr63gTjVTNsiGkgQhiyCShTgxt8oXOrklA==", - "dev": true, - "peer": true, - "requires": { - "tslib": "^2.0.1" - } - }, "ast-types-flow": { "version": "0.0.7", "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.7.tgz", @@ -21101,12 +11147,14 @@ "integrity": "sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==", "dev": true }, - "async-limiter": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz", - "integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==", + "asynciterator.prototype": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/asynciterator.prototype/-/asynciterator.prototype-1.0.0.tgz", + "integrity": "sha512-wwHYEIS0Q80f5mosx3L/dfG5t5rjEa9Ft51GTaNt862EnpyGHpgz2RkZvLPp1oF5TnAiTohkEKVEu8pQPJI7Vg==", "dev": true, - "peer": true + "requires": { + "has-symbols": "^1.0.3" + } }, "asynckit": { "version": "0.4.0", @@ -21117,14 +11165,8 @@ "at-least-node": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", - "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==" - }, - "atob": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", - "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==", - "dev": true, - "peer": true + "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", + "dev": true }, "authenticator": { "version": "1.1.5", @@ -21155,9 +11197,9 @@ "dev": true }, "aws-sdk": { - "version": "2.1371.0", - "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.1371.0.tgz", - "integrity": "sha512-dIts8xcmi7MFN1THOOcjiyqdvTqSVnsA4iIIerfyvpeYNobIi9pqBjCX9m8tfJ0pXQnymd4kp0xK9Y+i5dFfEQ==", + "version": "2.1468.0", + "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.1468.0.tgz", + "integrity": "sha512-1DAa0UA779M0VyKKPjTPDtox4KOyFrHnGDLbJrDKzqylC+O0DMR4kh9Oy2vfErhwFwsiPUvWgkknddVS+igVGA==", "dev": true, "requires": { "buffer": "4.9.2", @@ -21212,9 +11254,9 @@ "dev": true }, "axios": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.4.0.tgz", - "integrity": "sha512-S4XCWMEmzvo64T9GfvQDOXgYRDJ/wsSZc7Jvdgx5u1sd0JwsuPLqb3SYmusag+edF6ziyMensPVqLTSc1PiSEA==", + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.5.1.tgz", + "integrity": "sha512-Q28iYCWzNHjAm+yEAot5QaAMxhMghWLFVf7rRdwhUI+c2jix2DUXjAHXVi+s1ibs3mjPO/cCgbA++3BjD0vP/A==", "dev": true, "requires": { "follow-redirects": "^1.15.0", @@ -21242,9 +11284,9 @@ } }, "axios-retry": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/axios-retry/-/axios-retry-3.4.0.tgz", - "integrity": "sha512-VdgaP+gHH4iQYCCNUWF2pcqeciVOdGrBBAYUfTY+wPcO5Ltvp/37MLFNCmJKo7Gj3SHvCSdL8ouI1qLYJN3liA==", + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/axios-retry/-/axios-retry-3.8.0.tgz", + "integrity": "sha512-CfIsQyWNc5/AE7x/UEReRUadiBmQeoBpSEC+4QyGLJMswTsP1tz0GW2YYPnE7w9+ESMef5zOgLDFpHynNyEZ1w==", "dev": true, "requires": { "@babel/runtime": "^7.15.4", @@ -21260,146 +11302,11 @@ "deep-equal": "^2.0.5" } }, - "babel-core": { - "version": "7.0.0-bridge.0", - "resolved": "https://registry.npmjs.org/babel-core/-/babel-core-7.0.0-bridge.0.tgz", - "integrity": "sha512-poPX9mZH/5CSanm50Q+1toVci6pv5KSRv/5TWCwtzQS5XEwn40BcCrgIeMFWP9CKKIniKXNxoIOnOq4VVlGXhg==", - "dev": true, - "peer": true, - "requires": {} - }, - "babel-plugin-polyfill-corejs2": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.3.3.tgz", - "integrity": "sha512-8hOdmFYFSZhqg2C/JgLUQ+t52o5nirNwaWM2B9LWteozwIvM14VSwdsCAUET10qT+kmySAlseadmfeeSWFCy+Q==", - "dev": true, - "peer": true, - "requires": { - "@babel/compat-data": "^7.17.7", - "@babel/helper-define-polyfill-provider": "^0.3.3", - "semver": "^6.1.1" - } - }, - "babel-plugin-polyfill-corejs3": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.5.2.tgz", - "integrity": "sha512-G3uJih0XWiID451fpeFaYGVuxHEjzKTHtc9uGFEjR6hHrvNzeS/PX+LLLcetJcytsB5m4j+K3o/EpXJNb/5IEQ==", - "dev": true, - "peer": true, - "requires": { - "@babel/helper-define-polyfill-provider": "^0.3.1", - "core-js-compat": "^3.21.0" - } - }, - "babel-plugin-polyfill-regenerator": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.3.1.tgz", - "integrity": "sha512-Y2B06tvgHYt1x0yz17jGkGeeMr5FeKUu+ASJ+N6nB5lQ8Dapfg42i0OVrf8PNGJ3zKL4A23snMi1IRwrqqND7A==", - "dev": true, - "peer": true, - "requires": { - "@babel/helper-define-polyfill-provider": "^0.3.1" - } - }, - "babel-plugin-syntax-trailing-function-commas": { - "version": "7.0.0-beta.0", - "resolved": "https://registry.npmjs.org/babel-plugin-syntax-trailing-function-commas/-/babel-plugin-syntax-trailing-function-commas-7.0.0-beta.0.tgz", - "integrity": "sha512-Xj9XuRuz3nTSbaTXWv3itLOcxyF4oPD8douBBmj7U9BBC6nEBYfyOJYQMf/8PJAFotC62UY5dFfIGEPr7WswzQ==", - "dev": true, - "peer": true - }, - "babel-preset-fbjs": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/babel-preset-fbjs/-/babel-preset-fbjs-3.4.0.tgz", - "integrity": "sha512-9ywCsCvo1ojrw0b+XYk7aFvTH6D9064t0RIL1rtMf3nsa02Xw41MS7sZw216Im35xj/UY0PDBQsa1brUDDF1Ow==", - "dev": true, - "peer": true, - "requires": { - "@babel/plugin-proposal-class-properties": "^7.0.0", - "@babel/plugin-proposal-object-rest-spread": "^7.0.0", - "@babel/plugin-syntax-class-properties": "^7.0.0", - "@babel/plugin-syntax-flow": "^7.0.0", - "@babel/plugin-syntax-jsx": "^7.0.0", - "@babel/plugin-syntax-object-rest-spread": "^7.0.0", - "@babel/plugin-transform-arrow-functions": "^7.0.0", - "@babel/plugin-transform-block-scoped-functions": "^7.0.0", - "@babel/plugin-transform-block-scoping": "^7.0.0", - "@babel/plugin-transform-classes": "^7.0.0", - "@babel/plugin-transform-computed-properties": "^7.0.0", - "@babel/plugin-transform-destructuring": "^7.0.0", - "@babel/plugin-transform-flow-strip-types": "^7.0.0", - "@babel/plugin-transform-for-of": "^7.0.0", - "@babel/plugin-transform-function-name": "^7.0.0", - "@babel/plugin-transform-literals": "^7.0.0", - "@babel/plugin-transform-member-expression-literals": "^7.0.0", - "@babel/plugin-transform-modules-commonjs": "^7.0.0", - "@babel/plugin-transform-object-super": "^7.0.0", - "@babel/plugin-transform-parameters": "^7.0.0", - "@babel/plugin-transform-property-literals": "^7.0.0", - "@babel/plugin-transform-react-display-name": "^7.0.0", - "@babel/plugin-transform-react-jsx": "^7.0.0", - "@babel/plugin-transform-shorthand-properties": "^7.0.0", - "@babel/plugin-transform-spread": "^7.0.0", - "@babel/plugin-transform-template-literals": "^7.0.0", - "babel-plugin-syntax-trailing-function-commas": "^7.0.0-beta.0" - } - }, - "babel-runtime": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz", - "integrity": "sha512-ITKNuq2wKlW1fJg9sSW52eepoYgZBggvOAHC0u/CYu/qxQ9EVzThCgR69BnSXLHjy2f7SY5zaQ4yt7H9ZVxY2g==", - "dev": true, - "requires": { - "core-js": "^2.4.0", - "regenerator-runtime": "^0.11.0" - }, - "dependencies": { - "core-js": { - "version": "2.6.12", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.12.tgz", - "integrity": "sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ==", - "dev": true - }, - "regenerator-runtime": { - "version": "0.11.1", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz", - "integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==", - "dev": true - } - } - }, "balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" - }, - "base": { - "version": "0.11.2", - "resolved": "https://registry.npmjs.org/base/-/base-0.11.2.tgz", - "integrity": "sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==", - "dev": true, - "peer": true, - "requires": { - "cache-base": "^1.0.1", - "class-utils": "^0.3.5", - "component-emitter": "^1.2.1", - "define-property": "^1.0.0", - "isobject": "^3.0.1", - "mixin-deep": "^1.2.0", - "pascalcase": "^0.1.1" - }, - "dependencies": { - "define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha512-cZTYKFWspt9jZsMscWo8sc/5lbPC9Q0N5nBLgb+Yd915iL3udB1uFgS3B8YCx66UVHq018DAVFoee7x+gxggeA==", - "dev": true, - "peer": true, - "requires": { - "is-descriptor": "^1.0.0" - } - } - } + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true }, "base64-js": { "version": "1.5.1", @@ -21428,43 +11335,6 @@ "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", "dev": true }, - "bl": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", - "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", - "dev": true, - "peer": true, - "requires": { - "buffer": "^5.5.0", - "inherits": "^2.0.4", - "readable-stream": "^3.4.0" - }, - "dependencies": { - "buffer": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", - "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", - "dev": true, - "peer": true, - "requires": { - "base64-js": "^1.3.1", - "ieee754": "^1.1.13" - } - }, - "readable-stream": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", - "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", - "dev": true, - "peer": true, - "requires": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - } - } - } - }, "blob-util": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/blob-util/-/blob-util-2.0.2.tgz", @@ -21536,6 +11406,7 @@ "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -21545,6 +11416,7 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, "requires": { "fill-range": "^7.0.1" } @@ -21568,16 +11440,6 @@ "update-browserslist-db": "^1.0.10" } }, - "bser": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", - "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", - "dev": true, - "peer": true, - "requires": { - "node-int64": "^0.4.0" - } - }, "buffer": { "version": "4.9.2", "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.2.tgz", @@ -21620,24 +11482,6 @@ "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", "dev": true }, - "cache-base": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz", - "integrity": "sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==", - "dev": true, - "peer": true, - "requires": { - "collection-visit": "^1.0.0", - "component-emitter": "^1.2.1", - "get-value": "^2.0.6", - "has-value": "^1.0.0", - "isobject": "^3.0.1", - "set-value": "^2.0.0", - "to-object-path": "^0.3.0", - "union-value": "^1.0.0", - "unset-value": "^1.0.0" - } - }, "cachedir": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/cachedir/-/cachedir-2.3.0.tgz", @@ -21654,33 +11498,6 @@ "get-intrinsic": "^1.0.2" } }, - "caller-callsite": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/caller-callsite/-/caller-callsite-2.0.0.tgz", - "integrity": "sha512-JuG3qI4QOftFsZyOn1qq87fq5grLIyk1JYd5lJmdA+fG7aQ9pA/i3JIJGcO3q0MrRcHlOt1U+ZeHW8Dq9axALQ==", - "dev": true, - "peer": true, - "requires": { - "callsites": "^2.0.0" - } - }, - "caller-path": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/caller-path/-/caller-path-2.0.0.tgz", - "integrity": "sha512-MCL3sf6nCSXOwCTzvPKhN18TU7AHTvdtam8DAogxcrJ8Rjfbbg7Lgng64H9Iy+vUV6VGFClN/TyxBkAebLRR4A==", - "dev": true, - "peer": true, - "requires": { - "caller-callsite": "^2.0.0" - } - }, - "callsites": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-2.0.0.tgz", - "integrity": "sha512-ksWePWBloaWPxJYQ8TL0JHvtci6G5QTKwQ95RcWAa/lzoAKuAOflGdAK92hpHXjkwb8zLxoLNUoNYZgVsaJzvQ==", - "dev": true, - "peer": true - }, "camelcase": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.2.0.tgz", @@ -21701,24 +11518,25 @@ "dev": true }, "chai": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.7.tgz", - "integrity": "sha512-HLnAzZ2iupm25PlN0xFreAlBA5zaBSv3og0DdeGA4Ar6h6rJ3A0rolRUKJhSF2V10GZKDgWF/VmAEsNWjCRB+A==", + "version": "4.3.10", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.10.tgz", + "integrity": "sha512-0UXG04VuVbruMUYbJ6JctvH0YnC/4q3/AkT18q4NaITo91CUm0liMS9VqzT9vZhVQ/1eqPanMWjBM+Juhfb/9g==", "dev": true, "requires": { "assertion-error": "^1.1.0", - "check-error": "^1.0.2", - "deep-eql": "^4.1.2", - "get-func-name": "^2.0.0", - "loupe": "^2.3.1", + "check-error": "^1.0.3", + "deep-eql": "^4.1.3", + "get-func-name": "^2.0.2", + "loupe": "^2.3.6", "pathval": "^1.1.1", - "type-detect": "^4.0.5" + "type-detect": "^4.0.8" } }, "chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, "requires": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -21731,10 +11549,13 @@ "dev": true }, "check-error": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", - "integrity": "sha1-V00xLt2Iu13YkS6Sht1sCu1KrII=", - "dev": true + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz", + "integrity": "sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==", + "dev": true, + "requires": { + "get-func-name": "^2.0.2" + } }, "check-more-types": { "version": "2.24.0", @@ -21768,95 +11589,8 @@ "ci-info": { "version": "3.8.0", "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.8.0.tgz", - "integrity": "sha512-eXTggHWSooYhq49F2opQhuHWgzucfF2YgODK4e1566GQs5BIfP30B0oenwBJHfWxAs2fyPB1s7Mg949zLf61Yw==" - }, - "class-utils": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz", - "integrity": "sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==", - "dev": true, - "peer": true, - "requires": { - "arr-union": "^3.1.0", - "define-property": "^0.2.5", - "isobject": "^3.0.0", - "static-extend": "^0.1.1" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA==", - "dev": true, - "peer": true, - "requires": { - "is-descriptor": "^0.1.0" - } - }, - "is-accessor-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", - "integrity": "sha512-e1BM1qnDbMRG3ll2U9dSK0UMHuWOs3pY3AtcFsmvwPtKL3MML/Q86i+GilLfvqEs4GW+ExB91tQ3Ig9noDIZ+A==", - "dev": true, - "peer": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", - "dev": true, - "peer": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "is-data-descriptor": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", - "integrity": "sha512-+w9D5ulSoBNlmw9OHn3U2v51SyoCd0he+bB3xMl62oijhrspxowjU+AIcDY0N3iEJbUEkB15IlMASQsxYigvXg==", - "dev": true, - "peer": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", - "dev": true, - "peer": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "is-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", - "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", - "dev": true, - "peer": true, - "requires": { - "is-accessor-descriptor": "^0.1.6", - "is-data-descriptor": "^0.1.4", - "kind-of": "^5.0.0" - } - }, - "kind-of": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", - "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", - "dev": true, - "peer": true - } - } + "integrity": "sha512-eXTggHWSooYhq49F2opQhuHWgzucfF2YgODK4e1566GQs5BIfP30B0oenwBJHfWxAs2fyPB1s7Mg949zLf61Yw==", + "dev": true }, "clean-stack": { "version": "2.2.0", @@ -21883,13 +11617,6 @@ "restore-cursor": "^3.1.0" } }, - "cli-spinners": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.6.1.tgz", - "integrity": "sha512-x/5fWmGMnbKQAaNwN+UZlV79qBLM9JFnJuJ03gIi5whrob0xV0ofNVHy9DhwGdsMJQc2OKv0oGmLzvaqvAVv+g==", - "dev": true, - "peer": true - }, "cli-table3": { "version": "0.6.2", "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.2.tgz", @@ -21942,39 +11669,11 @@ } } }, - "clone": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.1.tgz", - "integrity": "sha512-h5FLmEMFHeuzqmpVRcDayNlVZ+k4uK1niyKQN6oUMe7ieJihv44Vc3dY/kDnnWX4PDQSwes48s965PG/D4GntQ==", - "dev": true - }, - "clone-deep": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", - "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", - "dev": true, - "peer": true, - "requires": { - "is-plain-object": "^2.0.4", - "kind-of": "^6.0.2", - "shallow-clone": "^3.0.0" - } - }, - "collection-visit": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz", - "integrity": "sha512-lNkKvzEeMBBjUGHZ+q6z9pSJla0KWAQPvtzhEV9+iGyQYG+pBpl7xKDhxoNSOZH2hhv0v5k0y2yAM4o4SjoSkw==", - "dev": true, - "peer": true, - "requires": { - "map-visit": "^1.0.0", - "object-visit": "^1.0.0" - } - }, "color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, "requires": { "color-name": "~1.1.4" } @@ -21982,7 +11681,8 @@ "color-name": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true }, "colorette": { "version": "1.4.0", @@ -21999,13 +11699,6 @@ "delayed-stream": "~1.0.0" } }, - "command-exists": { - "version": "1.2.9", - "resolved": "https://registry.npmjs.org/command-exists/-/command-exists-1.2.9.tgz", - "integrity": "sha512-LTQ/SGc+s0Xc0Fu5WaKnR0YiygZkm9eKFvyS+fRsU7/ZWFF8ykFM6Pc9aCVf1+xasOOZpO3BAVgVrKvsqKHV7w==", - "dev": true, - "peer": true - }, "commander": { "version": "6.2.1", "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz", @@ -22018,114 +11711,11 @@ "integrity": "sha512-6P6g0uetGpW/sdyUy/iQQCbFF0kWVMSIVSyYz7Zgjcgh8mgw8PQzDNZeyZ5DQ2gM7LBoZPHmnjz8rUthkBG5tw==", "dev": true }, - "commondir": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", - "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==", - "dev": true, - "peer": true - }, - "component-emitter": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz", - "integrity": "sha512-jPatnhd33viNplKjqXKRkGU345p263OIWzDL2wH3LGIGp5Kojo+uXizHmOADRvhGFFTnJqX3jBAKP6vvmSDKcA==", - "dev": true - }, - "compressible": { - "version": "2.0.18", - "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", - "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", - "dev": true, - "peer": true, - "requires": { - "mime-db": ">= 1.43.0 < 2" - } - }, - "compression": { - "version": "1.7.4", - "resolved": "https://registry.npmjs.org/compression/-/compression-1.7.4.tgz", - "integrity": "sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ==", - "dev": true, - "peer": true, - "requires": { - "accepts": "~1.3.5", - "bytes": "3.0.0", - "compressible": "~2.0.16", - "debug": "2.6.9", - "on-headers": "~1.0.2", - "safe-buffer": "5.1.2", - "vary": "~1.1.2" - }, - "dependencies": { - "bytes": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", - "integrity": "sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==", - "dev": true, - "peer": true - }, - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "peer": true, - "requires": { - "ms": "2.0.0" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true, - "peer": true - }, - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true, - "peer": true - } - } - }, "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" - }, - "connect": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/connect/-/connect-3.7.0.tgz", - "integrity": "sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ==", - "dev": true, - "peer": true, - "requires": { - "debug": "2.6.9", - "finalhandler": "1.1.2", - "parseurl": "~1.3.3", - "utils-merge": "1.0.1" - }, - "dependencies": { - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "peer": true, - "requires": { - "ms": "2.0.0" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true, - "peer": true - } - } + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true }, "content-disposition": { "version": "0.5.4", @@ -22173,71 +11763,12 @@ "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=", "dev": true }, - "copy-descriptor": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz", - "integrity": "sha512-XgZ0pFcakEUlbwQEVNg3+QAis1FyTL3Qel9FYy8pSkQqoG3PNoT0bOCQtOXcOkur21r2Eq2kI+IE+gsmAEVlYw==", - "dev": true, - "peer": true - }, - "core-js": { - "version": "3.8.3", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.8.3.tgz", - "integrity": "sha512-KPYXeVZYemC2TkNEkX/01I+7yd+nX3KddKwZ1Ww7SKWdI2wQprSgLmrTddT8nw92AjEklTsPBoSdQBhbI1bQ6Q==", - "dev": true - }, - "core-js-compat": { - "version": "3.29.1", - "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.29.1.tgz", - "integrity": "sha512-QmchCua884D8wWskMX8tW5ydINzd8oSJVx38lx/pVkFGqztxt73GYre3pm/hyYq8bPf+MW5In4I/uRShFDsbrA==", - "dev": true, - "peer": true, - "requires": { - "browserslist": "^4.21.5" - } - }, "core-util-is": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", "dev": true }, - "cosmiconfig": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-5.2.1.tgz", - "integrity": "sha512-H65gsXo1SKjf8zmrJ67eJk8aIRKV5ff2D4uKZIBZShbhGSpEmsQOPW/SKMKYhSTrqR7ufy6RP69rPogdaPh/kA==", - "dev": true, - "peer": true, - "requires": { - "import-fresh": "^2.0.0", - "is-directory": "^0.3.1", - "js-yaml": "^3.13.1", - "parse-json": "^4.0.0" - }, - "dependencies": { - "argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "dev": true, - "peer": true, - "requires": { - "sprintf-js": "~1.0.2" - } - }, - "js-yaml": { - "version": "3.14.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", - "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", - "dev": true, - "peer": true, - "requires": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" - } - } - } - }, "cross-env": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz", @@ -22251,6 +11782,7 @@ "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, "requires": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -22270,14 +11802,14 @@ "dev": true }, "cypress": { - "version": "12.11.0", - "resolved": "https://registry.npmjs.org/cypress/-/cypress-12.11.0.tgz", - "integrity": "sha512-TJE+CCWI26Hwr5Msb9GpQhFLubdYooW0fmlPwTsfiyxmngqc7+SZGLPeIkj2dTSSZSEtpQVzOzvcnzH0o8G7Vw==", + "version": "13.3.0", + "resolved": "https://registry.npmjs.org/cypress/-/cypress-13.3.0.tgz", + "integrity": "sha512-mpI8qcTwLGiA4zEQvTC/U1xGUezVV4V8HQCOYjlEOrVmU1etVvxOjkCXHGwrlYdZU/EPmUiWfsO3yt1o+Q2bgw==", "dev": true, "requires": { - "@cypress/request": "^2.88.10", + "@cypress/request": "^3.0.0", "@cypress/xvfb": "^1.2.4", - "@types/node": "^14.14.31", + "@types/node": "^18.17.5", "@types/sinonjs__fake-timers": "8.1.1", "@types/sizzle": "^2.3.2", "arch": "^2.2.0", @@ -22310,9 +11842,10 @@ "minimist": "^1.2.8", "ospath": "^1.2.2", "pretty-bytes": "^5.6.0", + "process": "^0.11.10", "proxy-from-env": "1.0.0", "request-progress": "^3.0.0", - "semver": "^7.3.2", + "semver": "^7.5.3", "supports-color": "^8.1.1", "tmp": "~0.2.1", "untildify": "^4.0.0", @@ -22330,9 +11863,9 @@ } }, "semver": { - "version": "7.3.7", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz", - "integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==", + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", "dev": true, "requires": { "lru-cache": "^6.0.0" @@ -22376,9 +11909,9 @@ } }, "cypress-wait-until": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/cypress-wait-until/-/cypress-wait-until-1.7.2.tgz", - "integrity": "sha512-uZ+M8/MqRcpf+FII/UZrU7g1qYZ4aVlHcgyVopnladyoBrpoaMJ4PKZDrdOJ05H5RHbr7s9Tid635X3E+ZLU/Q==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/cypress-wait-until/-/cypress-wait-until-2.0.1.tgz", + "integrity": "sha512-+IyVnYNiaX1+C+V/LazrJWAi/CqiwfNoRSrFviECQEyolW1gDRy765PZosL2alSSGK8V10Y7BGfOQyZUDgmnjQ==", "dev": true }, "damerau-levenshtein": { @@ -22403,9 +11936,9 @@ "dev": true }, "dayjs": { - "version": "1.11.7", - "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.7.tgz", - "integrity": "sha512-+Yw9U6YO5TQohxLcIkrXBeY73WP3ejHWVvx8XCk3gxvQDCTEmS48ZrSZCKciI7Bhl/uCMyxYtE9UqRILmFphkQ==", + "version": "1.11.10", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.10.tgz", + "integrity": "sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ==", "dev": true }, "debug": { @@ -22423,17 +11956,10 @@ "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==", "dev": true }, - "decode-uri-component": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", - "integrity": "sha512-hjf+xovcEn31w/EUYdTXQh/8smFL/dzYjohQGEIgjyNavaJfBY2p5F527Bo1VPATxv0VYTUC2bOcXvqFwk78Og==", - "dev": true, - "peer": true - }, "deep-eql": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.2.tgz", - "integrity": "sha512-gT18+YW4CcW/DBNTwAmqTtkJh7f9qqScu2qFVlx7kCoeY9tlBu9cUcr7+I+Z/noG8INehS3xQgLpTtd/QUTn4w==", + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.3.tgz", + "integrity": "sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw==", "dev": true, "requires": { "type-detect": "^4.0.0" @@ -22484,59 +12010,34 @@ "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", "dev": true }, - "defaults": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.3.tgz", - "integrity": "sha512-s82itHOnYrN0Ib8r+z7laQz3sdE+4FP3d9Q7VLO7U+KRT+CR0GsWuyHxzdAY82I7cXv0G/twrqomTJLOssO5HA==", + "define-data-property": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.0.tgz", + "integrity": "sha512-UzGwzcjyv3OtAvolTj1GoyNYzfFR+iqbGjcnBEENZVCpM4/Ng1yhGNvS3lR/xDS74Tb2wGG9WzNSNIOS9UVb2g==", "dev": true, - "peer": true, "requires": { - "clone": "^1.0.2" - }, - "dependencies": { - "clone": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", - "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", - "dev": true, - "peer": true - } + "get-intrinsic": "^1.2.1", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.0" } }, "define-properties": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.0.tgz", - "integrity": "sha512-xvqAVKGfT1+UAvPwKTVw/njhdQ8ZhXK4lI0bCIuCMrp2up9nPnaDftrLtmpTazqd1o+UY4zgzU+avtMbDP+ldA==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", "dev": true, "requires": { + "define-data-property": "^1.0.1", "has-property-descriptors": "^1.0.0", "object-keys": "^1.1.1" } }, - "define-property": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", - "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==", - "dev": true, - "peer": true, - "requires": { - "is-descriptor": "^1.0.2", - "isobject": "^3.0.1" - } - }, "delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", "dev": true }, - "denodeify": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/denodeify/-/denodeify-1.2.1.tgz", - "integrity": "sha512-KNTihKNmQENUZeKu5fzfpzRqR5S2VMp4gl9RFHiWzj9DfvYQPMJ6XHKNaQxaGCXwPk6y9yme3aUoaiAe+KX+vg==", - "dev": true, - "peer": true - }, "depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -22574,15 +12075,15 @@ } }, "dom-accessibility-api": { - "version": "0.5.14", - "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.14.tgz", - "integrity": "sha512-NMt+m9zFMPZe0JcY9gN224Qvk6qLIdqex29clBvc/y75ZBX9YA9wNK3frsYvu2DI1xcCIwxwnX+TlsJ2DSOADg==", + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", + "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", "dev": true }, "dotenv": { - "version": "16.0.3", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.0.3.tgz", - "integrity": "sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ==", + "version": "16.3.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.3.1.tgz", + "integrity": "sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ==", "dev": true }, "ecc-jsbn": { @@ -22649,57 +12150,20 @@ "ansi-colors": "^4.1.1" } }, - "envinfo": { - "version": "7.8.1", - "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.8.1.tgz", - "integrity": "sha512-/o+BXHmB7ocbHEAs6F2EnG0ogybVVUdkRunTT2glZU9XAaGmhqskrvKwqXuDfNjEO0LZKWdejEEpnq8aM0tOaw==", - "dev": true, - "peer": true - }, - "error-ex": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", - "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", - "dev": true, - "peer": true, - "requires": { - "is-arrayish": "^0.2.1" - } - }, - "error-stack-parser": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/error-stack-parser/-/error-stack-parser-2.1.4.tgz", - "integrity": "sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ==", - "dev": true, - "peer": true, - "requires": { - "stackframe": "^1.3.4" - } - }, - "errorhandler": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/errorhandler/-/errorhandler-1.5.1.tgz", - "integrity": "sha512-rcOwbfvP1WTViVoUjcfZicVzjhjTuhSMntHh6mW3IrEiyE6mJyXvsToJUJGlGlw/2xU9P5whlWNGlIDVeCiT4A==", - "dev": true, - "peer": true, - "requires": { - "accepts": "~1.3.7", - "escape-html": "~1.0.3" - } - }, "es-abstract": { - "version": "1.21.2", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.21.2.tgz", - "integrity": "sha512-y/B5POM2iBnIxCiernH1G7rC9qQoM77lLIMQLuob0zhp8C56Po81+2Nj0WFKnd0pNReDTnkYryc+zhOzpEIROg==", + "version": "1.22.2", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.22.2.tgz", + "integrity": "sha512-YoxfFcDmhjOgWPWsV13+2RNjq1F6UQnfs+8TftwNqtzlmFzEXvlUwdrNrYeaizfjQzRMxkZ6ElWMOJIFKdVqwA==", "dev": true, "requires": { "array-buffer-byte-length": "^1.0.0", + "arraybuffer.prototype.slice": "^1.0.2", "available-typed-arrays": "^1.0.5", "call-bind": "^1.0.2", "es-set-tostringtag": "^2.0.1", "es-to-primitive": "^1.2.1", - "function.prototype.name": "^1.1.5", - "get-intrinsic": "^1.2.0", + "function.prototype.name": "^1.1.6", + "get-intrinsic": "^1.2.1", "get-symbol-description": "^1.0.0", "globalthis": "^1.0.3", "gopd": "^1.0.1", @@ -22714,19 +12178,23 @@ "is-regex": "^1.1.4", "is-shared-array-buffer": "^1.0.2", "is-string": "^1.0.7", - "is-typed-array": "^1.1.10", + "is-typed-array": "^1.1.12", "is-weakref": "^1.0.2", "object-inspect": "^1.12.3", "object-keys": "^1.1.1", "object.assign": "^4.1.4", - "regexp.prototype.flags": "^1.4.3", + "regexp.prototype.flags": "^1.5.1", + "safe-array-concat": "^1.0.1", "safe-regex-test": "^1.0.0", - "string.prototype.trim": "^1.2.7", - "string.prototype.trimend": "^1.0.6", - "string.prototype.trimstart": "^1.0.6", + "string.prototype.trim": "^1.2.8", + "string.prototype.trimend": "^1.0.7", + "string.prototype.trimstart": "^1.0.7", + "typed-array-buffer": "^1.0.0", + "typed-array-byte-length": "^1.0.0", + "typed-array-byte-offset": "^1.0.0", "typed-array-length": "^1.0.4", "unbox-primitive": "^1.0.2", - "which-typed-array": "^1.1.9" + "which-typed-array": "^1.1.11" } }, "es-get-iterator": { @@ -22754,6 +12222,28 @@ } } }, + "es-iterator-helpers": { + "version": "1.0.15", + "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.0.15.tgz", + "integrity": "sha512-GhoY8uYqd6iwUl2kgjTm4CZAf6oo5mHK7BPqx3rKgx893YSsy0LGHV6gfqqQvZt/8xM8xeOnfXBCfqclMKkJ5g==", + "dev": true, + "requires": { + "asynciterator.prototype": "^1.0.0", + "call-bind": "^1.0.2", + "define-properties": "^1.2.1", + "es-abstract": "^1.22.1", + "es-set-tostringtag": "^2.0.1", + "function-bind": "^1.1.1", + "get-intrinsic": "^1.2.1", + "globalthis": "^1.0.3", + "has-property-descriptors": "^1.0.0", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "internal-slot": "^1.0.5", + "iterator.prototype": "^1.1.2", + "safe-array-concat": "^1.0.1" + } + }, "es-module-lexer": { "version": "0.9.3", "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-0.9.3.tgz", @@ -22811,27 +12301,27 @@ "dev": true }, "eslint": { - "version": "8.39.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.39.0.tgz", - "integrity": "sha512-mwiok6cy7KTW7rBpo05k6+p4YVZByLNjAZ/ACB9DRCu4YDRwjXI01tWHp6KAUWelsBetTxKK/2sHB0vdS8Z2Og==", + "version": "8.50.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.50.0.tgz", + "integrity": "sha512-FOnOGSuFuFLv/Sa+FDVRZl4GGVAAFFi8LecRsI5a1tMO5HIE8nCm4ivAlzt4dT3ol/PaaGC0rJEEXQmHJBGoOg==", "dev": true, "requires": { "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.4.0", - "@eslint/eslintrc": "^2.0.2", - "@eslint/js": "8.39.0", - "@humanwhocodes/config-array": "^0.11.8", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.2", + "@eslint/js": "8.50.0", + "@humanwhocodes/config-array": "^0.11.11", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", - "ajv": "^6.10.0", + "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.2", "debug": "^4.3.2", "doctrine": "^3.0.0", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^7.2.0", - "eslint-visitor-keys": "^3.4.0", - "espree": "^9.5.1", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", "esquery": "^1.4.2", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", @@ -22839,22 +12329,19 @@ "find-up": "^5.0.0", "glob-parent": "^6.0.2", "globals": "^13.19.0", - "grapheme-splitter": "^1.0.4", + "graphemer": "^1.4.0", "ignore": "^5.2.0", - "import-fresh": "^3.0.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "is-path-inside": "^3.0.3", - "js-sdsl": "^4.1.4", "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", + "optionator": "^0.9.3", "strip-ansi": "^6.0.1", - "strip-json-comments": "^3.1.0", "text-table": "^0.2.0" }, "dependencies": { @@ -22865,9 +12352,9 @@ "dev": true }, "eslint-scope": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.0.tgz", - "integrity": "sha512-DYj5deGlHBfMt15J7rdtyKNq/Nqlv5KfU4iodrQ019XESsRnwXH9KAE0y3cwtUHDo2ob7CypAnCqefh6vioWRw==", + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", "dev": true, "requires": { "esrecurse": "^4.3.0", @@ -22892,22 +12379,6 @@ "type-fest": "^0.20.2" } }, - "import-fresh": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", - "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", - "dev": true, - "requires": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - } - }, - "resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true - }, "strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", @@ -22959,22 +12430,22 @@ } }, "eslint-import-resolver-webpack": { - "version": "0.13.2", - "resolved": "https://registry.npmjs.org/eslint-import-resolver-webpack/-/eslint-import-resolver-webpack-0.13.2.tgz", - "integrity": "sha512-XodIPyg1OgE2h5BDErz3WJoK7lawxKTJNhgPNafRST6csC/MZC+L5P6kKqsZGRInpbgc02s/WZMrb4uGJzcuRg==", + "version": "0.13.7", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-webpack/-/eslint-import-resolver-webpack-0.13.7.tgz", + "integrity": "sha512-2a+meyMeABBRO4K53Oj1ygkmt5lhQS79Lmx2f684Qnv6gjvD4RLOM5jfPGTXwQ0A2K03WSoKt3HRQu/uBgxF7w==", "dev": true, "requires": { - "array-find": "^1.0.0", + "array.prototype.find": "^2.2.1", "debug": "^3.2.7", "enhanced-resolve": "^0.9.1", "find-root": "^1.1.0", "has": "^1.0.3", "interpret": "^1.4.0", - "is-core-module": "^2.7.0", + "is-core-module": "^2.13.0", "is-regex": "^1.1.4", "lodash": "^4.17.21", - "resolve": "^1.20.0", - "semver": "^5.7.1" + "resolve": "^2.0.0-next.4", + "semver": "^5.7.2" }, "dependencies": { "debug": { @@ -22992,18 +12463,29 @@ "integrity": "sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==", "dev": true }, + "resolve": { + "version": "2.0.0-next.4", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.4.tgz", + "integrity": "sha512-iMDbmAWtfU+MHpxt/I5iWI7cY6YVEZUQ3MBgPQ++XD1PELuJHIl82xBmObyP2KyQmkNB2dsqF7seoQQiAn5yDQ==", + "dev": true, + "requires": { + "is-core-module": "^2.9.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + } + }, "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", "dev": true } } }, "eslint-module-utils": { - "version": "2.7.4", - "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.7.4.tgz", - "integrity": "sha512-j4GT+rqzCoRKHwURX7pddtIPGySnX9Si/cgMI5ztrcqOPtk5dDEeZ34CQVPphnqkJytlc97Vuk05Um2mJ3gEQA==", + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.8.0.tgz", + "integrity": "sha512-aWajIYfsqCKRDgUfjEXNN/JlrzauMuSEy5sbd7WXbtW3EH6A6MpwEh42c7qD+MqQo9QMJ6fWLAeIJynx0g6OAw==", "dev": true, "requires": { "debug": "^3.2.7" @@ -23021,12 +12503,29 @@ } }, "eslint-plugin-cypress": { - "version": "2.13.3", - "resolved": "https://registry.npmjs.org/eslint-plugin-cypress/-/eslint-plugin-cypress-2.13.3.tgz", - "integrity": "sha512-nAPjZE5WopCsgJwl3vHm5iafpV+ZRO76Z9hMyRygWhmg5ODXDPd+9MaPl7kdJ2azj+sO87H3P1PRnggIrz848g==", + "version": "2.15.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-cypress/-/eslint-plugin-cypress-2.15.1.tgz", + "integrity": "sha512-eLHLWP5Q+I4j2AWepYq0PgFEei9/s5LvjuSqWrxurkg1YZ8ltxdvMNmdSf0drnsNo57CTgYY/NIHHLRSWejR7w==", "dev": true, "requires": { - "globals": "^11.12.0" + "globals": "^13.20.0" + }, + "dependencies": { + "globals": { + "version": "13.21.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.21.0.tgz", + "integrity": "sha512-ybyme3s4yy/t/3s35bewwXKOf7cvzfreG2lH0lZl0JB7I4GxRP2ghxOK/Nb9EkRXdbBXZLfq/p/0W2JUONB/Gg==", + "dev": true, + "requires": { + "type-fest": "^0.20.2" + } + }, + "type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true + } } }, "eslint-plugin-header": { @@ -23037,26 +12536,28 @@ "requires": {} }, "eslint-plugin-import": { - "version": "2.27.5", - "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.27.5.tgz", - "integrity": "sha512-LmEt3GVofgiGuiE+ORpnvP+kAm3h6MLZJ4Q5HCyHADofsb4VzXFsRiWj3c0OFiV+3DWFh0qg3v9gcPlfc3zRow==", + "version": "2.28.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.28.1.tgz", + "integrity": "sha512-9I9hFlITvOV55alzoKBI+K9q74kv0iKMeY6av5+umsNwayt59fz692daGyjR+oStBQgx6nwR9rXldDev3Clw+A==", "dev": true, "requires": { "array-includes": "^3.1.6", + "array.prototype.findlastindex": "^1.2.2", "array.prototype.flat": "^1.3.1", "array.prototype.flatmap": "^1.3.1", "debug": "^3.2.7", "doctrine": "^2.1.0", "eslint-import-resolver-node": "^0.3.7", - "eslint-module-utils": "^2.7.4", + "eslint-module-utils": "^2.8.0", "has": "^1.0.3", - "is-core-module": "^2.11.0", + "is-core-module": "^2.13.0", "is-glob": "^4.0.3", "minimatch": "^3.1.2", + "object.fromentries": "^2.0.6", + "object.groupby": "^1.0.0", "object.values": "^1.1.6", - "resolve": "^1.22.1", - "semver": "^6.3.0", - "tsconfig-paths": "^3.14.1" + "semver": "^6.3.1", + "tsconfig-paths": "^3.14.2" }, "dependencies": { "debug": { @@ -23077,16 +12578,11 @@ "esutils": "^2.0.2" } }, - "resolve": { - "version": "1.22.1", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz", - "integrity": "sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==", - "dev": true, - "requires": { - "is-core-module": "^2.9.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - } + "semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true } } }, @@ -23139,15 +12635,16 @@ "dev": true }, "eslint-plugin-react": { - "version": "7.32.2", - "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.32.2.tgz", - "integrity": "sha512-t2fBMa+XzonrrNkyVirzKlvn5RXzzPwRHtMvLAtVZrt8oxgnTQaYbU6SXTOO1mwQgp1y5+toMSKInnzGr0Knqg==", + "version": "7.33.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.33.2.tgz", + "integrity": "sha512-73QQMKALArI8/7xGLNI/3LylrEYrlKZSb5C9+q3OtOewTnMQi5cT+aE9E41sLCmli3I9PGGmD1yiZydyo4FEPw==", "dev": true, "requires": { "array-includes": "^3.1.6", "array.prototype.flatmap": "^1.3.1", "array.prototype.tosorted": "^1.1.1", "doctrine": "^2.1.0", + "es-iterator-helpers": "^1.0.12", "estraverse": "^5.3.0", "jsx-ast-utils": "^2.4.1 || ^3.0.0", "minimatch": "^3.1.2", @@ -23157,7 +12654,7 @@ "object.values": "^1.1.6", "prop-types": "^15.8.1", "resolve": "^2.0.0-next.4", - "semver": "^6.3.0", + "semver": "^6.3.1", "string.prototype.matchall": "^4.0.8" }, "dependencies": { @@ -23180,6 +12677,12 @@ "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" } + }, + "semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true } } }, @@ -23208,9 +12711,9 @@ } }, "eslint-visitor-keys": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.0.tgz", - "integrity": "sha512-HPpKPUBQcAsZOsHAFwTtIKcYlCje62XB7SEAcxjtmW6TD1WVpkS6i6/hOVtTZIl4zGj/mBqpFVGvaDneik+VoQ==", + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", "dev": true }, "esm": { @@ -23220,23 +12723,16 @@ "dev": true }, "espree": { - "version": "9.5.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.5.1.tgz", - "integrity": "sha512-5yxtHSZXRSW5pvv3hAlXM5+/Oswi1AUFqBmbibKb5s6bp3rGIDkyXU6xCoyuuLhijr4SFwPrXRoZjz0AZDN9tg==", + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", "dev": true, "requires": { - "acorn": "^8.8.0", + "acorn": "^8.9.0", "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^3.4.0" + "eslint-visitor-keys": "^3.4.1" } }, - "esprima": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "dev": true, - "peer": true - }, "esquery": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", @@ -23273,13 +12769,6 @@ "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=", "dev": true }, - "event-target-shim": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", - "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", - "dev": true, - "peer": true - }, "eventemitter2": { "version": "6.4.7", "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-6.4.7.tgz", @@ -23324,131 +12813,6 @@ "integrity": "sha1-BjJjj42HfMghB9MKD/8aF8uhzQw=", "dev": true }, - "expand-brackets": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", - "integrity": "sha512-w/ozOKR9Obk3qoWeY/WDi6MFta9AoMR+zud60mdnbniMcBxRuFJyDt2LdX/14A1UABeqk+Uk+LDfUpvoGKppZA==", - "dev": true, - "peer": true, - "requires": { - "debug": "^2.3.3", - "define-property": "^0.2.5", - "extend-shallow": "^2.0.1", - "posix-character-classes": "^0.1.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - }, - "dependencies": { - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "peer": true, - "requires": { - "ms": "2.0.0" - } - }, - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA==", - "dev": true, - "peer": true, - "requires": { - "is-descriptor": "^0.1.0" - } - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", - "dev": true, - "peer": true, - "requires": { - "is-extendable": "^0.1.0" - } - }, - "is-accessor-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", - "integrity": "sha512-e1BM1qnDbMRG3ll2U9dSK0UMHuWOs3pY3AtcFsmvwPtKL3MML/Q86i+GilLfvqEs4GW+ExB91tQ3Ig9noDIZ+A==", - "dev": true, - "peer": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", - "dev": true, - "peer": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "is-data-descriptor": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", - "integrity": "sha512-+w9D5ulSoBNlmw9OHn3U2v51SyoCd0he+bB3xMl62oijhrspxowjU+AIcDY0N3iEJbUEkB15IlMASQsxYigvXg==", - "dev": true, - "peer": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", - "dev": true, - "peer": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "is-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", - "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", - "dev": true, - "peer": true, - "requires": { - "is-accessor-descriptor": "^0.1.6", - "is-data-descriptor": "^0.1.4", - "kind-of": "^5.0.0" - } - }, - "is-extendable": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", - "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", - "dev": true, - "peer": true - }, - "kind-of": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", - "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", - "dev": true, - "peer": true - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true, - "peer": true - } - } - }, "express": { "version": "4.18.2", "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz", @@ -23550,63 +12914,6 @@ "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", "dev": true }, - "extend-shallow": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", - "integrity": "sha512-BwY5b5Ql4+qZoefgMj2NUmx+tehVTH/Kf4k1ZEtOHNFcm2wSxMRo992l6X3TIgni2eZVTZ85xMOjF31fwZAj6Q==", - "dev": true, - "peer": true, - "requires": { - "assign-symbols": "^1.0.0", - "is-extendable": "^1.0.1" - } - }, - "extglob": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", - "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", - "dev": true, - "peer": true, - "requires": { - "array-unique": "^0.3.2", - "define-property": "^1.0.0", - "expand-brackets": "^2.1.4", - "extend-shallow": "^2.0.1", - "fragment-cache": "^0.2.1", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - }, - "dependencies": { - "define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha512-cZTYKFWspt9jZsMscWo8sc/5lbPC9Q0N5nBLgb+Yd915iL3udB1uFgS3B8YCx66UVHq018DAVFoee7x+gxggeA==", - "dev": true, - "peer": true, - "requires": { - "is-descriptor": "^1.0.0" - } - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", - "dev": true, - "peer": true, - "requires": { - "is-extendable": "^0.1.0" - } - }, - "is-extendable": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", - "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", - "dev": true, - "peer": true - } - } - }, "extract-zip": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", @@ -23632,9 +12939,9 @@ "dev": true }, "fast-glob": { - "version": "3.2.12", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.12.tgz", - "integrity": "sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz", + "integrity": "sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==", "dev": true, "requires": { "@nodelib/fs.stat": "^2.0.2", @@ -23665,16 +12972,6 @@ "reusify": "^1.0.4" } }, - "fb-watchman": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.1.tgz", - "integrity": "sha512-DkPJKQeY6kKwmuMretBhr7G6Vodr7bFwDYTXIkfG1gjvNpaxBTQV3PbXg6bR1c1UP4jPOX0jHUbbHANL9vRjVg==", - "dev": true, - "peer": true, - "requires": { - "bser": "2.1.1" - } - }, "fd-slicer": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", @@ -23706,57 +13003,11 @@ "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, "requires": { "to-regex-range": "^5.0.1" } }, - "finalhandler": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", - "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", - "dev": true, - "peer": true, - "requires": { - "debug": "2.6.9", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "on-finished": "~2.3.0", - "parseurl": "~1.3.3", - "statuses": "~1.5.0", - "unpipe": "~1.0.0" - }, - "dependencies": { - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "peer": true, - "requires": { - "ms": "2.0.0" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true, - "peer": true - } - } - }, - "find-cache-dir": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-2.1.0.tgz", - "integrity": "sha512-Tq6PixE0w/VMFfCgbONnkiQIVol/JJL7nRMi20fqzA4NRs9AfeqMGeRdPi3wIhYkxjeBaWh2rxwapn5Tu3IqOQ==", - "dev": true, - "peer": true, - "requires": { - "commondir": "^1.0.1", - "make-dir": "^2.0.0", - "pkg-dir": "^3.0.0" - } - }, "find-root": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", @@ -23773,14 +13024,6 @@ "path-exists": "^4.0.0" } }, - "find-yarn-workspace-root": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/find-yarn-workspace-root/-/find-yarn-workspace-root-2.0.0.tgz", - "integrity": "sha512-1IMnbjt4KzsQfnhnzNd8wUEgXZ44IzZaZmnLYx7D5FZlaHt2gW20Cri8Q+E/t5tIj4+epTBub+2Zxu/vNILzqQ==", - "requires": { - "micromatch": "^4.0.2" - } - }, "flat": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", @@ -23814,13 +13057,6 @@ "integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==", "dev": true }, - "flow-parser": { - "version": "0.121.0", - "resolved": "https://registry.npmjs.org/flow-parser/-/flow-parser-0.121.0.tgz", - "integrity": "sha512-1gIBiWJNR0tKUNv8gZuk7l9rVX06OuLzY9AoGio7y/JT4V1IZErEMEq2TJS+PFcw/y0RshZ1J/27VfK1UQzYVg==", - "dev": true, - "peer": true - }, "follow-redirects": { "version": "1.15.1", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.1.tgz", @@ -23836,13 +13072,6 @@ "is-callable": "^1.1.3" } }, - "for-in": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", - "integrity": "sha512-7EwmXrOjyL+ChxMhmG5lnW9MPt1aIeZEwKhQzoBUdTV0N3zuwWDZYVJatDvZ2OyzPUvdIAZDsCetk3coyMfcnQ==", - "dev": true, - "peer": true - }, "forever-agent": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", @@ -23866,16 +13095,6 @@ "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", "dev": true }, - "fragment-cache": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz", - "integrity": "sha512-GMBAbW9antB8iZRHLoGw0b3HANt57diZYFO/HL1JGIC1MjKrdmhxvrJbupnVvpys0zsz7yBApXdQyfepKly2kA==", - "dev": true, - "peer": true, - "requires": { - "map-cache": "^0.2.2" - } - }, "fresh": { "version": "0.5.2", "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", @@ -23886,6 +13105,7 @@ "version": "9.1.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, "requires": { "at-least-node": "^1.0.0", "graceful-fs": "^4.2.0", @@ -23896,7 +13116,8 @@ "fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "dev": true }, "fsevents": { "version": "2.3.2", @@ -23918,15 +13139,15 @@ "dev": true }, "function.prototype.name": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.5.tgz", - "integrity": "sha512-uN7m/BzVKQnCUF/iW8jYea67v++2u7m5UgENbHRtdDVclOUP+FMPlCNdmk0h/ysGyo2tavMJEDqJAkJdRa1vMA==", + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.6.tgz", + "integrity": "sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg==", "dev": true, "requires": { "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "es-abstract": "^1.19.0", - "functions-have-names": "^1.2.2" + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "functions-have-names": "^1.2.3" } }, "functions-have-names": { @@ -23949,19 +13170,20 @@ "dev": true }, "get-func-name": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", - "integrity": "sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", + "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==", "dev": true }, "get-intrinsic": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.0.tgz", - "integrity": "sha512-L049y6nFOuom5wGyRc3/gdTLO94dySVKRACj1RmJZBQXlbTMhtNIgkWkUHq+jYmZvKf14EW1EoJnnjbmoHij0Q==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz", + "integrity": "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==", "dev": true, "requires": { "function-bind": "^1.1.1", "has": "^1.0.3", + "has-proto": "^1.0.1", "has-symbols": "^1.0.3" } }, @@ -23971,12 +13193,6 @@ "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", "dev": true }, - "get-params": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/get-params/-/get-params-0.1.2.tgz", - "integrity": "sha512-41eOxtlGgHQRbFyA8KTH+w+32Em3cRdfBud7j67ulzmIfmaHX9doq47s0fa4P5o9H64BZX9nrYI6sJvk46Op+Q==", - "dev": true - }, "get-stream": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", @@ -23996,13 +13212,6 @@ "get-intrinsic": "^1.1.1" } }, - "get-value": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", - "integrity": "sha512-Ln0UQDlxH1BapMu3GPtf7CuYNwRZf2gwCuPqbyG6pB8WfmFpzqcy4xtAaAMUhnNqjMKTiCPZG2oMT3YSx8U2NA==", - "dev": true, - "peer": true - }, "getopts": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/getopts/-/getopts-2.3.0.tgz", @@ -24027,19 +13236,11 @@ "assert-plus": "^1.0.0" } }, - "gfycat-sdk": { - "version": "1.4.18", - "resolved": "https://registry.npmjs.org/gfycat-sdk/-/gfycat-sdk-1.4.18.tgz", - "integrity": "sha512-BrINtO6rj8Nr0pm38Qr3epayOuvlKcEFcDCw6yL9T4SrsWTECct6FS/isli766kdij2GGG6bWU6bRp+fDS2Cbg==", - "dev": true, - "requires": { - "babel-runtime": "^6.23.0" - } - }, "glob": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", + "dev": true, "requires": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -24078,7 +13279,8 @@ "version": "11.12.0", "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", - "dev": true + "dev": true, + "peer": true }, "globalthis": { "version": "1.0.3", @@ -24115,12 +13317,13 @@ "graceful-fs": { "version": "4.2.10", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", - "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==" + "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==", + "dev": true }, - "grapheme-splitter": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz", - "integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==", + "graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", "dev": true }, "has": { @@ -24141,7 +13344,8 @@ "has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true }, "has-property-descriptors": { "version": "1.0.0", @@ -24173,103 +13377,12 @@ "has-symbols": "^1.0.2" } }, - "has-value": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz", - "integrity": "sha512-IBXk4GTsLYdQ7Rvt+GRBrFSVEkmuOUy4re0Xjd9kJSUQpnTrWR4/y9RpfexN9vkAPMFuQoeWKwqzPozRTlasGw==", - "dev": true, - "peer": true, - "requires": { - "get-value": "^2.0.6", - "has-values": "^1.0.0", - "isobject": "^3.0.0" - } - }, - "has-values": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-values/-/has-values-1.0.0.tgz", - "integrity": "sha512-ODYZC64uqzmtfGMEAX/FvZiRyWLpAC3vYnNunURUnkGVTS+mI0smVsWaPydRBsE3g+ok7h960jChO8mFcWlHaQ==", - "dev": true, - "peer": true, - "requires": { - "is-number": "^3.0.0", - "kind-of": "^4.0.0" - }, - "dependencies": { - "is-number": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", - "integrity": "sha512-4cboCqIpliH+mAvFNegjZQ4kgKc3ZUhQVr3HvWbSh5q3WH2v82ct+T2Y1hdU5Gdtorx/cLifQjqCbL7bpznLTg==", - "dev": true, - "peer": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", - "dev": true, - "peer": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "kind-of": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz", - "integrity": "sha512-24XsCxmEbRwEDbz/qz3stgin8TTzZ1ESR56OMCN0ujYg+vRutNSiOj9bHH9u85DKgXguraugV5sFuvbD4FW/hw==", - "dev": true, - "peer": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, "he": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", "dev": true }, - "hermes-engine": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/hermes-engine/-/hermes-engine-0.11.0.tgz", - "integrity": "sha512-7aMUlZja2IyLYAcZ69NBnwJAR5ZOYlSllj0oMpx08a8HzxHOys0eKCzfphrf6D0vX1JGO1QQvVsQKe6TkYherw==", - "dev": true, - "peer": true - }, - "hermes-estree": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.6.0.tgz", - "integrity": "sha512-2YTGzJCkhdmT6VuNprWjXnvTvw/3iPNw804oc7yknvQpNKo+vJGZmtvLLCghOZf0OwzKaNAzeIMp71zQbNl09w==", - "dev": true, - "peer": true - }, - "hermes-parser": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.6.0.tgz", - "integrity": "sha512-Vf58jBZca2+QBLR9h7B7mdg8oFz2g5ILz1iVouZ5DOrOrAfBmPfJjdjDT8jrO0f+iJ4/hSRrQHqHIjSnTaLUDQ==", - "dev": true, - "peer": true, - "requires": { - "hermes-estree": "0.6.0" - } - }, - "hermes-profile-transformer": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/hermes-profile-transformer/-/hermes-profile-transformer-0.0.6.tgz", - "integrity": "sha512-cnN7bQUm65UWOy6cbGcCcZ3rpwW8Q/j4OP5aWRhEry4Z2t2aR1cjrbp0BS+KiBN0smvP1caBgAuxutvyvJILzQ==", - "dev": true, - "peer": true, - "requires": { - "source-map": "^0.7.3" - } - }, "http-errors": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", @@ -24329,30 +13442,12 @@ "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==", "dev": true }, - "image-size": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/image-size/-/image-size-0.6.3.tgz", - "integrity": "sha512-47xSUiQioGaB96nqtp5/q55m0aBQSQdyIloMOc/x+QVTDZLNmXE892IIDrJ0hM1A5vcNUDD5tDffkSP5lCaIIA==", - "dev": true, - "peer": true - }, "immediate": { "version": "3.0.6", "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", "integrity": "sha1-nbHb0Pr43m++D13V5Wu2BigN5ps=", "dev": true }, - "import-fresh": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-2.0.0.tgz", - "integrity": "sha512-eZ5H8rcgYazHbKC3PG4ClHNykCSxtAhxSSEM+2mb+7evD2CKF5V7c0dNum7AdpDh0ZdICwZY9sRSn8f+KH96sg==", - "dev": true, - "peer": true, - "requires": { - "caller-path": "^2.0.0", - "resolve-from": "^3.0.0" - } - }, "imurmurhash": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", @@ -24369,6 +13464,7 @@ "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dev": true, "requires": { "once": "^1.3.0", "wrappy": "1" @@ -24377,7 +13473,8 @@ "inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true }, "ini": { "version": "2.0.0", @@ -24402,23 +13499,6 @@ "integrity": "sha512-Ju0Bz/cEia55xDwUWEa8+olFpCiQoypjnQySseKtmjNrnps3P+xfpUmGr90T7yjlVJmOtybRvPXhKMbHr+fWnw==", "dev": true }, - "invariant": { - "version": "2.2.4", - "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", - "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", - "dev": true, - "peer": true, - "requires": { - "loose-envify": "^1.0.0" - } - }, - "ip": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.8.tgz", - "integrity": "sha512-PuExPYUiu6qMBQb4l06ecm6T6ujzhmh+MeJcW9wa89PoAz5pvd4zPgN5WJV104mb6S2T1AwNIAaB70JNrLQWhg==", - "dev": true, - "peer": true - }, "ip-regex": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-2.1.0.tgz", @@ -24431,16 +13511,6 @@ "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", "dev": true }, - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "dev": true, - "peer": true, - "requires": { - "kind-of": "^6.0.0" - } - }, "is-arguments": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz", @@ -24462,12 +13532,14 @@ "is-typed-array": "^1.1.10" } }, - "is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "is-async-function": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.0.0.tgz", + "integrity": "sha512-Y1JXKrfykRJGdlDwdKlLpLyMIiWqWvuSd17TvZk68PLAOGOoF4Xyav1z0Xhoi+gCYjZVeC5SI+hYFOfvXmGRCA==", "dev": true, - "peer": true + "requires": { + "has-tostringtag": "^1.0.0" + } }, "is-bigint": { "version": "1.0.4", @@ -24519,24 +13591,14 @@ } }, "is-core-module": { - "version": "2.11.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.11.0.tgz", - "integrity": "sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw==", + "version": "2.13.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.0.tgz", + "integrity": "sha512-Z7dk6Qo8pOCp3l4tsX2C5ZVas4V+UxwQodwZhLopL91TX8UyyHEXafPcyoeeWuLrwzHcr3igO78wNLwHJHsMCQ==", "dev": true, "requires": { "has": "^1.0.3" } }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "dev": true, - "peer": true, - "requires": { - "kind-of": "^6.0.0" - } - }, "is-date-object": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", @@ -24546,46 +13608,21 @@ "has-tostringtag": "^1.0.0" } }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "dev": true, - "peer": true, - "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - } - }, - "is-directory": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/is-directory/-/is-directory-0.3.1.tgz", - "integrity": "sha512-yVChGzahRFvbkscn2MlwGismPO12i9+znNruC5gVEntG3qu0xQMzsGg/JFbrsqDOHtHFPci+V5aP5T9I+yeKqw==", - "dev": true, - "peer": true - }, - "is-docker": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", - "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==" - }, - "is-extendable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", - "dev": true, - "peer": true, - "requires": { - "is-plain-object": "^2.0.4" - } - }, "is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", "dev": true }, + "is-finalizationregistry": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.0.2.tgz", + "integrity": "sha512-0by5vtUJs8iFQb5TYUHHPudOR+qXYIMKtiUzvLIZITZUjknFmziyBJuLhVRc+Ds0dREFlskDNJKYIdIzu/9pfw==", + "dev": true, + "requires": { + "call-bind": "^1.0.2" + } + }, "is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", @@ -24620,13 +13657,6 @@ "is-path-inside": "^3.0.2" } }, - "is-interactive": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", - "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", - "dev": true, - "peer": true - }, "is-map": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.2.tgz", @@ -24642,7 +13672,8 @@ "is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==" + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true }, "is-number-object": { "version": "1.0.7", @@ -24665,16 +13696,6 @@ "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", "dev": true }, - "is-plain-object": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", - "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", - "dev": true, - "peer": true, - "requires": { - "isobject": "^3.0.1" - } - }, "is-regex": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", @@ -24731,16 +13752,12 @@ } }, "is-typed-array": { - "version": "1.1.10", - "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.10.tgz", - "integrity": "sha512-PJqgEHiWZvMpaFZ3uTc8kHPM4+4ADTlDniuQL7cU/UDA0Ql7F70yGfHph3cLNe+c9toaigv+DFzTJKhc2CtO6A==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.12.tgz", + "integrity": "sha512-Z14TF2JNG8Lss5/HMqt0//T9JeHXttXy5pH/DBU4vi98ozO2btxzq9MwYDZYnKwU8nRsz/+GVFVRDq3DkVuSPg==", "dev": true, "requires": { - "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.2", - "for-each": "^0.3.3", - "gopd": "^1.0.1", - "has-tostringtag": "^1.0.0" + "which-typed-array": "^1.1.11" } }, "is-typedarray": { @@ -24780,20 +13797,6 @@ "get-intrinsic": "^1.1.1" } }, - "is-windows": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", - "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", - "dev": true, - "peer": true - }, - "is-wsl": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-1.1.0.tgz", - "integrity": "sha512-gfygJYZ2gLTDlmbWMI0CE2MwnFzSN/2SZfkMlItC4K/JBlsWVDB0bO6XhqcY13YXE7iMcAJnzTCJjPiTeJJ0Mw==", - "dev": true, - "peer": true - }, "isarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", @@ -24803,14 +13806,8 @@ "isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" - }, - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", - "dev": true, - "peer": true + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", + "dev": true }, "isstream": { "version": "0.1.2", @@ -24818,107 +13815,17 @@ "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=", "dev": true }, - "jest-get-type": { - "version": "26.3.0", - "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-26.3.0.tgz", - "integrity": "sha512-TpfaviN1R2pQWkIihlfEanwOXK0zcxrKEE4MlU6Tn7keoXdN6/3gK/xl0yEh8DOunn5pOVGKf8hB4R9gVh04ig==", + "iterator.prototype": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.2.tgz", + "integrity": "sha512-DR33HMMr8EzwuRL8Y9D3u2BMj8+RqSE850jfGu59kS7tbmPLzGkZmVSfyCFSDxuZiEY6Rzt3T2NA/qU+NwVj1w==", "dev": true, - "peer": true - }, - "jest-haste-map": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-27.5.1.tgz", - "integrity": "sha512-7GgkZ4Fw4NFbMSDSpZwXeBiIbx+t/46nJ2QitkOjvwPYyZmqttu2TDSimMHP1EkPOi4xUZAN1doE5Vd25H4Jng==", - "dev": true, - "peer": true, "requires": { - "@jest/types": "^27.5.1", - "@types/graceful-fs": "^4.1.2", - "@types/node": "*", - "anymatch": "^3.0.3", - "fb-watchman": "^2.0.0", - "fsevents": "^2.3.2", - "graceful-fs": "^4.2.9", - "jest-regex-util": "^27.5.1", - "jest-serializer": "^27.5.1", - "jest-util": "^27.5.1", - "jest-worker": "^27.5.1", - "micromatch": "^4.0.4", - "walker": "^1.0.7" - } - }, - "jest-regex-util": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-27.5.1.tgz", - "integrity": "sha512-4bfKq2zie+x16okqDXjXn9ql2B0dScQu+vcwe4TvFVhkVyuWLqpZrZtXxLLWoXYgn0E87I6r6GRYHF7wFZBUvg==", - "dev": true, - "peer": true - }, - "jest-serializer": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-serializer/-/jest-serializer-27.5.1.tgz", - "integrity": "sha512-jZCyo6iIxO1aqUxpuBlwTDMkzOAJS4a3eYz3YzgxxVQFwLeSA7Jfq5cbqCY+JLvTDrWirgusI/0KwxKMgrdf7w==", - "dev": true, - "peer": true, - "requires": { - "@types/node": "*", - "graceful-fs": "^4.2.9" - } - }, - "jest-util": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-27.5.1.tgz", - "integrity": "sha512-Kv2o/8jNvX1MQ0KGtw480E/w4fBCDOnH6+6DmeKi6LZUIlKA5kwY0YNdlzaWTiVgxqAqik11QyxDOKk543aKXw==", - "dev": true, - "peer": true, - "requires": { - "@jest/types": "^27.5.1", - "@types/node": "*", - "chalk": "^4.0.0", - "ci-info": "^3.2.0", - "graceful-fs": "^4.2.9", - "picomatch": "^2.2.3" - } - }, - "jest-validate": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-26.6.2.tgz", - "integrity": "sha512-NEYZ9Aeyj0i5rQqbq+tpIOom0YS1u2MVu6+euBsvpgIme+FOfRmoC4R5p0JiAUpaFvFy24xgrpMknarR/93XjQ==", - "dev": true, - "peer": true, - "requires": { - "@jest/types": "^26.6.2", - "camelcase": "^6.0.0", - "chalk": "^4.0.0", - "jest-get-type": "^26.3.0", - "leven": "^3.1.0", - "pretty-format": "^26.6.2" - }, - "dependencies": { - "@jest/types": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-26.6.2.tgz", - "integrity": "sha512-fC6QCp7Sc5sX6g8Tvbmj4XUTbyrik0akgRy03yjXbQaBWWNWGE7SGtJk98m0N8nzegD/7SggrUlivxo5ax4KWQ==", - "dev": true, - "peer": true, - "requires": { - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^15.0.0", - "chalk": "^4.0.0" - } - }, - "@types/yargs": { - "version": "15.0.14", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.14.tgz", - "integrity": "sha512-yEJzHoxf6SyQGhBhIYGXQDSCkJjB6HohDShto7m8vaKg9Yp0Yn8+71J9eakh2bnPg6BfsH9PRMhiRTZnd4eXGQ==", - "dev": true, - "peer": true, - "requires": { - "@types/yargs-parser": "*" - } - } + "define-properties": "^1.2.1", + "get-intrinsic": "^1.2.1", + "has-symbols": "^1.0.3", + "reflect.getprototypeof": "^1.0.4", + "set-function-name": "^2.0.1" } }, "jest-worker": { @@ -24945,39 +13852,12 @@ } } }, - "jetifier": { - "version": "1.6.8", - "resolved": "https://registry.npmjs.org/jetifier/-/jetifier-1.6.8.tgz", - "integrity": "sha512-3Zi16h6L5tXDRQJTb221cnRoVG9/9OvreLdLU2/ZjRv/GILL+2Cemt0IKvkowwkDpvouAU1DQPOJ7qaiHeIdrw==", - "dev": true, - "peer": true - }, "jmespath": { "version": "0.16.0", "resolved": "https://registry.npmjs.org/jmespath/-/jmespath-0.16.0.tgz", "integrity": "sha512-9FzQjJ7MATs1tSpnco1K6ayiYE3figslrXA72G2HQ/n76RzvYlofyi5QM+iX4YRs/pu3yzxlVQSST23+dMDknw==", "dev": true }, - "joi": { - "version": "17.6.0", - "resolved": "https://registry.npmjs.org/joi/-/joi-17.6.0.tgz", - "integrity": "sha512-OX5dG6DTbcr/kbMFj0KGYxuew69HPcAE3K/sZpEV2nP6e/j/C0HV+HNiBPCASxdx5T7DMoa0s8UeHWMnb6n2zw==", - "dev": true, - "peer": true, - "requires": { - "@hapi/hoek": "^9.0.0", - "@hapi/topo": "^5.0.0", - "@sideway/address": "^4.1.3", - "@sideway/formula": "^3.0.0", - "@sideway/pinpoint": "^2.0.0" - } - }, - "js-sdsl": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.4.0.tgz", - "integrity": "sha512-FfVSdx6pJ41Oa+CF7RDaFmTnCaFhua+SNYQX74riGOpl96x+2jQCqEfQ2bnXu/5DPCqlRuiqyvTJM0Qjz26IVg==", - "dev": true - }, "js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -24993,193 +13873,12 @@ "argparse": "^2.0.1" } }, - "jsan": { - "version": "3.1.14", - "resolved": "https://registry.npmjs.org/jsan/-/jsan-3.1.14.tgz", - "integrity": "sha512-wStfgOJqMv4QKktuH273f5fyi3D3vy2pHOiSDGPvpcS/q+wb/M7AK3vkCcaHbkZxDOlDU/lDJgccygKSG2OhtA==", - "dev": true - }, "jsbn": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", "integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==", "dev": true }, - "jsc-android": { - "version": "250230.2.1", - "resolved": "https://registry.npmjs.org/jsc-android/-/jsc-android-250230.2.1.tgz", - "integrity": "sha512-KmxeBlRjwoqCnBBKGsihFtvsBHyUFlBxJPK4FzeYcIuBfdjv6jFys44JITAgSTbQD+vIdwMEfyZklsuQX0yI1Q==", - "dev": true, - "peer": true - }, - "jscodeshift": { - "version": "0.13.1", - "resolved": "https://registry.npmjs.org/jscodeshift/-/jscodeshift-0.13.1.tgz", - "integrity": "sha512-lGyiEbGOvmMRKgWk4vf+lUrCWO/8YR8sUR3FKF1Cq5fovjZDlIcw3Hu5ppLHAnEXshVffvaM0eyuY/AbOeYpnQ==", - "dev": true, - "peer": true, - "requires": { - "@babel/core": "^7.13.16", - "@babel/parser": "^7.13.16", - "@babel/plugin-proposal-class-properties": "^7.13.0", - "@babel/plugin-proposal-nullish-coalescing-operator": "^7.13.8", - "@babel/plugin-proposal-optional-chaining": "^7.13.12", - "@babel/plugin-transform-modules-commonjs": "^7.13.8", - "@babel/preset-flow": "^7.13.13", - "@babel/preset-typescript": "^7.13.0", - "@babel/register": "^7.13.16", - "babel-core": "^7.0.0-bridge.0", - "chalk": "^4.1.2", - "flow-parser": "0.*", - "graceful-fs": "^4.2.4", - "micromatch": "^3.1.10", - "neo-async": "^2.5.0", - "node-dir": "^0.1.17", - "recast": "^0.20.4", - "temp": "^0.8.4", - "write-file-atomic": "^2.3.0" - }, - "dependencies": { - "braces": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", - "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", - "dev": true, - "peer": true, - "requires": { - "arr-flatten": "^1.1.0", - "array-unique": "^0.3.2", - "extend-shallow": "^2.0.1", - "fill-range": "^4.0.0", - "isobject": "^3.0.1", - "repeat-element": "^1.1.2", - "snapdragon": "^0.8.1", - "snapdragon-node": "^2.0.1", - "split-string": "^3.0.2", - "to-regex": "^3.0.1" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", - "dev": true, - "peer": true, - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "fill-range": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", - "integrity": "sha512-VcpLTWqWDiTerugjj8e3+esbg+skS3M9e54UuR3iCeIDMXCLTsAH8hTSzDQU/X6/6t3eYkOKoZSef2PlU6U1XQ==", - "dev": true, - "peer": true, - "requires": { - "extend-shallow": "^2.0.1", - "is-number": "^3.0.0", - "repeat-string": "^1.6.1", - "to-regex-range": "^2.1.0" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", - "dev": true, - "peer": true, - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "is-extendable": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", - "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", - "dev": true, - "peer": true - }, - "is-number": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", - "integrity": "sha512-4cboCqIpliH+mAvFNegjZQ4kgKc3ZUhQVr3HvWbSh5q3WH2v82ct+T2Y1hdU5Gdtorx/cLifQjqCbL7bpznLTg==", - "dev": true, - "peer": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", - "dev": true, - "peer": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "micromatch": { - "version": "3.1.10", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", - "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", - "dev": true, - "peer": true, - "requires": { - "arr-diff": "^4.0.0", - "array-unique": "^0.3.2", - "braces": "^2.3.1", - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "extglob": "^2.0.4", - "fragment-cache": "^0.2.1", - "kind-of": "^6.0.2", - "nanomatch": "^1.2.9", - "object.pick": "^1.3.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.2" - } - }, - "rimraf": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", - "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", - "dev": true, - "peer": true, - "requires": { - "glob": "^7.1.3" - } - }, - "temp": { - "version": "0.8.4", - "resolved": "https://registry.npmjs.org/temp/-/temp-0.8.4.tgz", - "integrity": "sha512-s0ZZzd0BzYv5tLSptZooSjK8oj6C+c19p7Vqta9+6NPOf7r+fxq0cJe6/oN4LTC79sy5NY8ucOJNgwsKCSbfqg==", - "dev": true, - "peer": true, - "requires": { - "rimraf": "~2.6.2" - } - }, - "to-regex-range": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", - "integrity": "sha512-ZZWNfCjUokXXDGXFpZehJIkZqq91BcULFq/Pi7M5i4JnxXdhMKAK682z8bCW3o8Hj1wuuzoKcW3DfVzaP6VuNg==", - "dev": true, - "peer": true, - "requires": { - "is-number": "^3.0.0", - "repeat-string": "^1.6.1" - } - } - } - }, "jsesc": { "version": "2.5.2", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", @@ -25187,13 +13886,6 @@ "dev": true, "peer": true }, - "json-parse-better-errors": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", - "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", - "dev": true, - "peer": true - }, "json-parse-even-better-errors": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", @@ -25236,6 +13928,7 @@ "version": "6.1.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dev": true, "requires": { "graceful-fs": "^4.1.6", "universalify": "^2.0.0" @@ -25263,46 +13956,14 @@ "object.assign": "^4.1.3" } }, - "kind-of": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", - "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", - "dev": true, - "peer": true - }, - "klaw": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/klaw/-/klaw-1.3.1.tgz", - "integrity": "sha512-TED5xi9gGQjGpNnvRWknrwAB1eL5GciPfVFOt3Vk1OJCVDQbzuSfrF3hkUQKlsgKrG1F+0t5W0m+Fje1jIt8rw==", - "dev": true, - "peer": true, - "requires": { - "graceful-fs": "^4.1.9" - } - }, - "klaw-sync": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/klaw-sync/-/klaw-sync-6.0.0.tgz", - "integrity": "sha512-nIeuVSzdCCs6TDPTqI8w1Yre34sSq7AkZ4B3sfOBbI2CgVSB4Du4aLQijFU2+lhAFCwt9+42Hel6lQNIv6AntQ==", - "requires": { - "graceful-fs": "^4.1.11" - } - }, - "kleur": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", - "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", - "dev": true, - "peer": true - }, "knex": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/knex/-/knex-2.4.2.tgz", - "integrity": "sha512-tMI1M7a+xwHhPxjbl/H9K1kHX+VncEYcvCx5K00M16bWvpYPKAZd6QrCu68PtHAdIZNQPWZn0GVhqVBEthGWCg==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/knex/-/knex-2.5.1.tgz", + "integrity": "sha512-z78DgGKUr4SE/6cm7ku+jHvFT0X97aERh/f0MUKAKgFnwCYBEW4TFBqtHWFYiJFid7fMrtpZ/gxJthvz5mEByA==", "dev": true, "requires": { "colorette": "2.0.19", - "commander": "^9.1.0", + "commander": "^10.0.0", "debug": "4.3.4", "escalade": "^3.1.1", "esm": "^3.2.25", @@ -25310,7 +13971,7 @@ "getopts": "2.3.0", "interpret": "^2.2.0", "lodash": "^4.17.21", - "pg-connection-string": "2.5.0", + "pg-connection-string": "2.6.1", "rechoir": "^0.8.0", "resolve-from": "^5.0.0", "tarn": "^3.0.2", @@ -25324,9 +13985,9 @@ "dev": true }, "commander": { - "version": "9.4.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-9.4.1.tgz", - "integrity": "sha512-5EEkTNyHNGFPD2H+c/dXXfQZYa/scCKasxWcXJaWnNJ99pnQN9Vnmqow+p+PlFPE63Q6mThaZws1T+HxfpgtPw==", + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", + "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", "dev": true }, "rechoir": { @@ -25367,13 +14028,6 @@ "integrity": "sha1-eZllXoZGwX8In90YfRUNMyTVRRM=", "dev": true }, - "leven": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", - "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", - "dev": true, - "peer": true - }, "levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -25393,12 +14047,6 @@ "immediate": "~3.0.5" } }, - "linked-list": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/linked-list/-/linked-list-0.1.0.tgz", - "integrity": "sha512-Zr4ovrd0ODzF3ut2TWZMdHIxb8iFdJc/P3QM4iCJdlxxGHXo69c9hGIHzLo8/FtuR9E6WUZc5irKhtPUgOKMAg==", - "dev": true - }, "listr2": { "version": "3.10.0", "resolved": "https://registry.npmjs.org/listr2/-/listr2-3.10.0.tgz", @@ -25445,19 +14093,6 @@ "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", "dev": true }, - "lodash-es": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", - "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==", - "dev": true - }, - "lodash.debounce": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", - "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", - "dev": true, - "peer": true - }, "lodash.intersection": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/lodash.intersection/-/lodash.intersection-4.4.0.tgz", @@ -25506,13 +14141,6 @@ "integrity": "sha1-DdOXEhPHxW34gJd9UEyI+0cal6w=", "dev": true }, - "lodash.throttle": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/lodash.throttle/-/lodash.throttle-4.1.1.tgz", - "integrity": "sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ==", - "dev": true, - "peer": true - }, "lodash.without": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/lodash.without/-/lodash.without-4.4.0.tgz", @@ -25580,147 +14208,6 @@ } } }, - "logkitty": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/logkitty/-/logkitty-0.7.1.tgz", - "integrity": "sha512-/3ER20CTTbahrCrpYfPn7Xavv9diBROZpoXGVZDWMw4b/X4uuUwAC0ki85tgsdMRONURyIJbcOvS94QsUBYPbQ==", - "dev": true, - "peer": true, - "requires": { - "ansi-fragments": "^0.2.1", - "dayjs": "^1.8.15", - "yargs": "^15.1.0" - }, - "dependencies": { - "camelcase": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", - "dev": true, - "peer": true - }, - "cliui": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", - "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", - "dev": true, - "peer": true, - "requires": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", - "wrap-ansi": "^6.2.0" - } - }, - "decamelize": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", - "dev": true, - "peer": true - }, - "find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, - "peer": true, - "requires": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - } - }, - "locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dev": true, - "peer": true, - "requires": { - "p-locate": "^4.1.0" - } - }, - "p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dev": true, - "peer": true, - "requires": { - "p-try": "^2.0.0" - } - }, - "p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dev": true, - "peer": true, - "requires": { - "p-limit": "^2.2.0" - } - }, - "strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "peer": true, - "requires": { - "ansi-regex": "^5.0.1" - } - }, - "wrap-ansi": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", - "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", - "dev": true, - "peer": true, - "requires": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - } - }, - "y18n": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", - "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", - "dev": true, - "peer": true - }, - "yargs": { - "version": "15.4.1", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", - "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", - "dev": true, - "peer": true, - "requires": { - "cliui": "^6.0.0", - "decamelize": "^1.2.0", - "find-up": "^4.1.0", - "get-caller-file": "^2.0.1", - "require-directory": "^2.1.1", - "require-main-filename": "^2.0.0", - "set-blocking": "^2.0.0", - "string-width": "^4.2.0", - "which-module": "^2.0.0", - "y18n": "^4.0.0", - "yargs-parser": "^18.1.2" - } - }, - "yargs-parser": { - "version": "18.1.3", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", - "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", - "dev": true, - "peer": true, - "requires": { - "camelcase": "^5.0.0", - "decamelize": "^1.2.0" - } - } - } - }, "loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -25731,9 +14218,9 @@ } }, "loupe": { - "version": "2.3.4", - "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.4.tgz", - "integrity": "sha512-OvKfgCC2Ndby6aSTREl5aCCPTNIzlDfQZvZxNUrBrihDhL3xcrYegTblhmEiCrg2kKQz4XsFIaemE5BF4ybSaQ==", + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.6.tgz", + "integrity": "sha512-RaPMZKiMy8/JruncMU5Bt6na1eftNoo++R4Y+N2FrxkDVTrGvcyzFTsaGif4QTeKESheMGegbhw6iUAq+5A8zA==", "dev": true, "requires": { "get-func-name": "^2.0.0" @@ -25749,38 +14236,11 @@ } }, "lz-string": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.4.4.tgz", - "integrity": "sha512-0ckx7ZHRPqb0oUm8zNr+90mtf9DQB60H1wMCjBtfi62Kl3a7JbHob6gA2bC+xRvZoOL+1hzUK8jeuEIQE8svEQ==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", "dev": true }, - "make-dir": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", - "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", - "dev": true, - "peer": true, - "requires": { - "pify": "^4.0.1", - "semver": "^5.6.0" - }, - "dependencies": { - "pify": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", - "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", - "dev": true, - "peer": true - }, - "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true, - "peer": true - } - } - }, "make-error": { "version": "1.3.6", "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", @@ -25796,78 +14256,6 @@ "make-error": "^1.3.5" } }, - "makeerror": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", - "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", - "dev": true, - "peer": true, - "requires": { - "tmpl": "1.0.5" - } - }, - "map-cache": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", - "integrity": "sha512-8y/eV9QQZCiyn1SprXSrCmqJN0yNRATe+PO8ztwqrvrbdRLA3eYJF0yaR0YayLWkMbsQSKWS9N2gPcGEc4UsZg==", - "dev": true, - "peer": true - }, - "map-visit": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/map-visit/-/map-visit-1.0.0.tgz", - "integrity": "sha512-4y7uGv8bd2WdM9vpQsiQNo41Ln1NvhvDRuVt0k2JZQ+ezN2uaQes7lZeZ+QQUHOLQAtDaBJ+7wCbi+ab/KFs+w==", - "dev": true, - "peer": true, - "requires": { - "object-visit": "^1.0.0" - } - }, - "mattermost-redux": { - "version": "5.33.1", - "resolved": "https://registry.npmjs.org/mattermost-redux/-/mattermost-redux-5.33.1.tgz", - "integrity": "sha512-fHTW5PeB0xAE5u6uMds+hlWaJsUeWgE1034vekUah1X4bZCzBcMK4fM+ycGaIQ7c5Ylj9ML6Ni10XJYiQpMo5w==", - "dev": true, - "requires": { - "core-js": "3.8.3", - "form-data": "3.0.0", - "gfycat-sdk": "1.4.18", - "moment-timezone": "0.5.32", - "redux": "4.0.5", - "redux-action-buffer": "1.2.0", - "redux-offline": "git+https://github.com/enahum/redux-offline.git#885024de96b6ec73650c340c8928066585c413df", - "redux-persist": "4.9.1", - "redux-persist-node-storage": "2.0.0", - "redux-thunk": "2.3.0", - "remote-redux-devtools": "0.5.16", - "reselect": "4.0.0", - "rudder-sdk-js": "1.0.14", - "serialize-error": "6.0.0", - "shallow-equals": "1.0.0" - }, - "dependencies": { - "form-data": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.0.tgz", - "integrity": "sha512-CKMFDglpbMi6PyN+brwB9Q/GOw0eAnsrEZDgcsH5Krhz5Od/haKHAX0NmQfha2zPPz0JpWzA7GJHGSnvCRLWsg==", - "dev": true, - "requires": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" - } - }, - "moment-timezone": { - "version": "0.5.32", - "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.32.tgz", - "integrity": "sha512-Z8QNyuQHQAmWucp8Knmgei8YNo28aLjJq6Ma+jy1ZSpSk5nyfRT8xgUbSQvD2+2UajISfenndwvFuH3NGS+nvA==", - "dev": true, - "requires": { - "moment": ">= 2.9.0" - } - } - } - }, "md5": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/md5/-/md5-2.3.0.tgz", @@ -25885,13 +14273,6 @@ "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", "dev": true }, - "memoize-one": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz", - "integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==", - "dev": true, - "peer": true - }, "memory-fs": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.2.0.tgz", @@ -25922,678 +14303,11 @@ "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=", "dev": true }, - "metro": { - "version": "0.70.3", - "resolved": "https://registry.npmjs.org/metro/-/metro-0.70.3.tgz", - "integrity": "sha512-uEWS7xg8oTetQDABYNtsyeUjdLhH3KAvLFpaFFoJqUpOk2A3iygszdqmjobFl6W4zrvKDJS+XxdMR1roYvUhTw==", - "dev": true, - "peer": true, - "requires": { - "@babel/code-frame": "^7.0.0", - "@babel/core": "^7.14.0", - "@babel/generator": "^7.14.0", - "@babel/parser": "^7.14.0", - "@babel/template": "^7.0.0", - "@babel/traverse": "^7.14.0", - "@babel/types": "^7.0.0", - "absolute-path": "^0.0.0", - "accepts": "^1.3.7", - "async": "^3.2.2", - "chalk": "^4.0.0", - "ci-info": "^2.0.0", - "connect": "^3.6.5", - "debug": "^2.2.0", - "denodeify": "^1.2.1", - "error-stack-parser": "^2.0.6", - "fs-extra": "^1.0.0", - "graceful-fs": "^4.2.4", - "hermes-parser": "0.6.0", - "image-size": "^0.6.0", - "invariant": "^2.2.4", - "jest-haste-map": "^27.3.1", - "jest-worker": "^27.2.0", - "lodash.throttle": "^4.1.1", - "metro-babel-transformer": "0.70.3", - "metro-cache": "0.70.3", - "metro-cache-key": "0.70.3", - "metro-config": "0.70.3", - "metro-core": "0.70.3", - "metro-hermes-compiler": "0.70.3", - "metro-inspector-proxy": "0.70.3", - "metro-minify-uglify": "0.70.3", - "metro-react-native-babel-preset": "0.70.3", - "metro-resolver": "0.70.3", - "metro-runtime": "0.70.3", - "metro-source-map": "0.70.3", - "metro-symbolicate": "0.70.3", - "metro-transform-plugins": "0.70.3", - "metro-transform-worker": "0.70.3", - "mime-types": "^2.1.27", - "node-fetch": "^2.2.0", - "nullthrows": "^1.1.1", - "rimraf": "^2.5.4", - "serialize-error": "^2.1.0", - "source-map": "^0.5.6", - "strip-ansi": "^6.0.0", - "temp": "0.8.3", - "throat": "^5.0.0", - "ws": "^7.5.1", - "yargs": "^15.3.1" - }, - "dependencies": { - "camelcase": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", - "dev": true, - "peer": true - }, - "ci-info": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz", - "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==", - "dev": true, - "peer": true - }, - "cliui": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", - "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", - "dev": true, - "peer": true, - "requires": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", - "wrap-ansi": "^6.2.0" - } - }, - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "peer": true, - "requires": { - "ms": "2.0.0" - } - }, - "decamelize": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", - "dev": true, - "peer": true - }, - "find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, - "peer": true, - "requires": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - } - }, - "fs-extra": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-1.0.0.tgz", - "integrity": "sha512-VerQV6vEKuhDWD2HGOybV6v5I73syoc/cXAbKlgTC7M/oFVEtklWlp9QH2Ijw3IaWDOQcMkldSPa7zXy79Z/UQ==", - "dev": true, - "peer": true, - "requires": { - "graceful-fs": "^4.1.2", - "jsonfile": "^2.1.0", - "klaw": "^1.0.0" - } - }, - "jsonfile": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-2.4.0.tgz", - "integrity": "sha512-PKllAqbgLgxHaj8TElYymKCAgrASebJrWpTnEkOaTowt23VKXXN0sUeriJ+eh7y6ufb/CC5ap11pz71/cM0hUw==", - "dev": true, - "peer": true, - "requires": { - "graceful-fs": "^4.1.6" - } - }, - "locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dev": true, - "peer": true, - "requires": { - "p-locate": "^4.1.0" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true, - "peer": true - }, - "p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dev": true, - "peer": true, - "requires": { - "p-try": "^2.0.0" - } - }, - "p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dev": true, - "peer": true, - "requires": { - "p-limit": "^2.2.0" - } - }, - "serialize-error": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-2.1.0.tgz", - "integrity": "sha512-ghgmKt5o4Tly5yEG/UJp8qTd0AN7Xalw4XBtDEKP655B699qMEtra1WlXeE6WIvdEG481JvRxULKsInq/iNysw==", - "dev": true, - "peer": true - }, - "source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", - "dev": true, - "peer": true - }, - "strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "peer": true, - "requires": { - "ansi-regex": "^5.0.1" - } - }, - "wrap-ansi": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", - "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", - "dev": true, - "peer": true, - "requires": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - } - }, - "ws": { - "version": "7.5.8", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.8.tgz", - "integrity": "sha512-ri1Id1WinAX5Jqn9HejiGb8crfRio0Qgu8+MtL36rlTA6RLsMdWt1Az/19A2Qij6uSHUMphEFaTKa4WG+UNHNw==", - "dev": true, - "peer": true, - "requires": {} - }, - "y18n": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", - "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", - "dev": true, - "peer": true - }, - "yargs": { - "version": "15.4.1", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", - "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", - "dev": true, - "peer": true, - "requires": { - "cliui": "^6.0.0", - "decamelize": "^1.2.0", - "find-up": "^4.1.0", - "get-caller-file": "^2.0.1", - "require-directory": "^2.1.1", - "require-main-filename": "^2.0.0", - "set-blocking": "^2.0.0", - "string-width": "^4.2.0", - "which-module": "^2.0.0", - "y18n": "^4.0.0", - "yargs-parser": "^18.1.2" - } - }, - "yargs-parser": { - "version": "18.1.3", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", - "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", - "dev": true, - "peer": true, - "requires": { - "camelcase": "^5.0.0", - "decamelize": "^1.2.0" - } - } - } - }, - "metro-babel-transformer": { - "version": "0.70.3", - "resolved": "https://registry.npmjs.org/metro-babel-transformer/-/metro-babel-transformer-0.70.3.tgz", - "integrity": "sha512-bWhZRMn+mIOR/s3BDpFevWScz9sV8FGktVfMlF1eJBLoX24itHDbXvTktKBYi38PWIKcHedh6THSFpJogfuwNA==", - "dev": true, - "peer": true, - "requires": { - "@babel/core": "^7.14.0", - "hermes-parser": "0.6.0", - "metro-source-map": "0.70.3", - "nullthrows": "^1.1.1" - } - }, - "metro-cache": { - "version": "0.70.3", - "resolved": "https://registry.npmjs.org/metro-cache/-/metro-cache-0.70.3.tgz", - "integrity": "sha512-iCix/+z812fUqa6KlOxaTkY6LQQDoXIe/VljXkGIvpygSCmYyhjQpfQVZEVVPezFmUBYXNdabdQ6cYx6JX3yMg==", - "dev": true, - "peer": true, - "requires": { - "metro-core": "0.70.3", - "rimraf": "^2.5.4" - } - }, - "metro-cache-key": { - "version": "0.70.3", - "resolved": "https://registry.npmjs.org/metro-cache-key/-/metro-cache-key-0.70.3.tgz", - "integrity": "sha512-0zpw+IcpM3hmGd5sKMdxNv3sbOIUYnMUvx1/yaM6vNRReSPmOLX0bP8fYf3CGgk8NEreZ1OHbVsuw7bdKt40Mw==", - "dev": true, - "peer": true - }, - "metro-config": { - "version": "0.70.3", - "resolved": "https://registry.npmjs.org/metro-config/-/metro-config-0.70.3.tgz", - "integrity": "sha512-SSCDjSTygoCgzoj61DdrBeJzZDRwQxUEfcgc6t6coxWSExXNR4mOngz0q4SAam49Bmjq9J2Jft6qUKnUTPrRgA==", - "dev": true, - "peer": true, - "requires": { - "cosmiconfig": "^5.0.5", - "jest-validate": "^26.5.2", - "metro": "0.70.3", - "metro-cache": "0.70.3", - "metro-core": "0.70.3", - "metro-runtime": "0.70.3" - } - }, - "metro-core": { - "version": "0.70.3", - "resolved": "https://registry.npmjs.org/metro-core/-/metro-core-0.70.3.tgz", - "integrity": "sha512-NzfHB/w5R7yLaOeU1tzPTbBzCRsYSvpKJkLMP0yudszKZzIAZqNdjoEJ9GZ688Wi0ynZxcU0BxukXh4my80ZBw==", - "dev": true, - "peer": true, - "requires": { - "jest-haste-map": "^27.3.1", - "lodash.throttle": "^4.1.1", - "metro-resolver": "0.70.3" - } - }, - "metro-hermes-compiler": { - "version": "0.70.3", - "resolved": "https://registry.npmjs.org/metro-hermes-compiler/-/metro-hermes-compiler-0.70.3.tgz", - "integrity": "sha512-W6WttLi4E72JL/NyteQ84uxYOFMibe0PUr9aBKuJxxfCq6QRnJKOVcNY0NLW0He2tneXGk+8ZsNz8c0flEvYqg==", - "dev": true, - "peer": true - }, - "metro-inspector-proxy": { - "version": "0.70.3", - "resolved": "https://registry.npmjs.org/metro-inspector-proxy/-/metro-inspector-proxy-0.70.3.tgz", - "integrity": "sha512-qQoNdPGrmyoJSWYkxSDpTaAI8xyqVdNDVVj9KRm1PG8niSuYmrCCFGLLFsMvkVYwsCWUGHoGBx0UoAzVp14ejw==", - "dev": true, - "peer": true, - "requires": { - "connect": "^3.6.5", - "debug": "^2.2.0", - "ws": "^7.5.1", - "yargs": "^15.3.1" - }, - "dependencies": { - "camelcase": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", - "dev": true, - "peer": true - }, - "cliui": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", - "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", - "dev": true, - "peer": true, - "requires": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", - "wrap-ansi": "^6.2.0" - } - }, - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "peer": true, - "requires": { - "ms": "2.0.0" - } - }, - "decamelize": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", - "dev": true, - "peer": true - }, - "find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, - "peer": true, - "requires": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - } - }, - "locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dev": true, - "peer": true, - "requires": { - "p-locate": "^4.1.0" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true, - "peer": true - }, - "p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dev": true, - "peer": true, - "requires": { - "p-try": "^2.0.0" - } - }, - "p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dev": true, - "peer": true, - "requires": { - "p-limit": "^2.2.0" - } - }, - "strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "peer": true, - "requires": { - "ansi-regex": "^5.0.1" - } - }, - "wrap-ansi": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", - "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", - "dev": true, - "peer": true, - "requires": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - } - }, - "ws": { - "version": "7.5.8", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.8.tgz", - "integrity": "sha512-ri1Id1WinAX5Jqn9HejiGb8crfRio0Qgu8+MtL36rlTA6RLsMdWt1Az/19A2Qij6uSHUMphEFaTKa4WG+UNHNw==", - "dev": true, - "peer": true, - "requires": {} - }, - "y18n": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", - "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", - "dev": true, - "peer": true - }, - "yargs": { - "version": "15.4.1", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", - "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", - "dev": true, - "peer": true, - "requires": { - "cliui": "^6.0.0", - "decamelize": "^1.2.0", - "find-up": "^4.1.0", - "get-caller-file": "^2.0.1", - "require-directory": "^2.1.1", - "require-main-filename": "^2.0.0", - "set-blocking": "^2.0.0", - "string-width": "^4.2.0", - "which-module": "^2.0.0", - "y18n": "^4.0.0", - "yargs-parser": "^18.1.2" - } - }, - "yargs-parser": { - "version": "18.1.3", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", - "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", - "dev": true, - "peer": true, - "requires": { - "camelcase": "^5.0.0", - "decamelize": "^1.2.0" - } - } - } - }, - "metro-minify-uglify": { - "version": "0.70.3", - "resolved": "https://registry.npmjs.org/metro-minify-uglify/-/metro-minify-uglify-0.70.3.tgz", - "integrity": "sha512-oHyjV9WDqOlDE1FPtvs6tIjjeY/oP1PNUPYL1wqyYtqvjN+zzAOrcbsAAL1sv+WARaeiMsWkF2bwtNo+Hghoog==", - "dev": true, - "peer": true, - "requires": { - "uglify-es": "^3.1.9" - } - }, - "metro-react-native-babel-preset": { - "version": "0.70.3", - "resolved": "https://registry.npmjs.org/metro-react-native-babel-preset/-/metro-react-native-babel-preset-0.70.3.tgz", - "integrity": "sha512-4Nxc1zEiHEu+GTdEMEsHnRgfaBkg8f/Td3+FcQ8NTSvs+xL3LBrQy6N07idWSQZHIdGFf+tTHvRfSIWLD8u8Tg==", - "dev": true, - "peer": true, - "requires": { - "@babel/core": "^7.14.0", - "@babel/plugin-proposal-async-generator-functions": "^7.0.0", - "@babel/plugin-proposal-class-properties": "^7.0.0", - "@babel/plugin-proposal-export-default-from": "^7.0.0", - "@babel/plugin-proposal-nullish-coalescing-operator": "^7.0.0", - "@babel/plugin-proposal-object-rest-spread": "^7.0.0", - "@babel/plugin-proposal-optional-catch-binding": "^7.0.0", - "@babel/plugin-proposal-optional-chaining": "^7.0.0", - "@babel/plugin-syntax-dynamic-import": "^7.0.0", - "@babel/plugin-syntax-export-default-from": "^7.0.0", - "@babel/plugin-syntax-flow": "^7.2.0", - "@babel/plugin-syntax-nullish-coalescing-operator": "^7.0.0", - "@babel/plugin-syntax-optional-chaining": "^7.0.0", - "@babel/plugin-transform-arrow-functions": "^7.0.0", - "@babel/plugin-transform-async-to-generator": "^7.0.0", - "@babel/plugin-transform-block-scoping": "^7.0.0", - "@babel/plugin-transform-classes": "^7.0.0", - "@babel/plugin-transform-computed-properties": "^7.0.0", - "@babel/plugin-transform-destructuring": "^7.0.0", - "@babel/plugin-transform-exponentiation-operator": "^7.0.0", - "@babel/plugin-transform-flow-strip-types": "^7.0.0", - "@babel/plugin-transform-function-name": "^7.0.0", - "@babel/plugin-transform-literals": "^7.0.0", - "@babel/plugin-transform-modules-commonjs": "^7.0.0", - "@babel/plugin-transform-named-capturing-groups-regex": "^7.0.0", - "@babel/plugin-transform-parameters": "^7.0.0", - "@babel/plugin-transform-react-display-name": "^7.0.0", - "@babel/plugin-transform-react-jsx": "^7.0.0", - "@babel/plugin-transform-react-jsx-self": "^7.0.0", - "@babel/plugin-transform-react-jsx-source": "^7.0.0", - "@babel/plugin-transform-runtime": "^7.0.0", - "@babel/plugin-transform-shorthand-properties": "^7.0.0", - "@babel/plugin-transform-spread": "^7.0.0", - "@babel/plugin-transform-sticky-regex": "^7.0.0", - "@babel/plugin-transform-template-literals": "^7.0.0", - "@babel/plugin-transform-typescript": "^7.5.0", - "@babel/plugin-transform-unicode-regex": "^7.0.0", - "@babel/template": "^7.0.0", - "react-refresh": "^0.4.0" - } - }, - "metro-react-native-babel-transformer": { - "version": "0.70.3", - "resolved": "https://registry.npmjs.org/metro-react-native-babel-transformer/-/metro-react-native-babel-transformer-0.70.3.tgz", - "integrity": "sha512-WKBU6S/G50j9cfmFM4k4oRYprd8u3qjleD4so1E2zbTNILg+gYla7ZFGCAvi2G0ZcqS2XuGCR375c2hF6VVvwg==", - "dev": true, - "peer": true, - "requires": { - "@babel/core": "^7.14.0", - "babel-preset-fbjs": "^3.4.0", - "hermes-parser": "0.6.0", - "metro-babel-transformer": "0.70.3", - "metro-react-native-babel-preset": "0.70.3", - "metro-source-map": "0.70.3", - "nullthrows": "^1.1.1" - } - }, - "metro-resolver": { - "version": "0.70.3", - "resolved": "https://registry.npmjs.org/metro-resolver/-/metro-resolver-0.70.3.tgz", - "integrity": "sha512-5Pc5S/Gs4RlLbziuIWtvtFd9GRoILlaRC8RZDVq5JZWcWHywKy/PjNmOBNhpyvtRlzpJfy/ssIfLhu8zINt1Mw==", - "dev": true, - "peer": true, - "requires": { - "absolute-path": "^0.0.0" - } - }, - "metro-runtime": { - "version": "0.70.3", - "resolved": "https://registry.npmjs.org/metro-runtime/-/metro-runtime-0.70.3.tgz", - "integrity": "sha512-22xU7UdXZacniTIDZgN2EYtmfau2pPyh97Dcs+cWrLcJYgfMKjWBtesnDcUAQy3PHekDYvBdJZkoQUeskYTM+w==", - "dev": true, - "peer": true, - "requires": { - "@babel/runtime": "^7.0.0" - } - }, - "metro-source-map": { - "version": "0.70.3", - "resolved": "https://registry.npmjs.org/metro-source-map/-/metro-source-map-0.70.3.tgz", - "integrity": "sha512-zsYtZGrwRbbGEFHtmMqqeCH9K9aTGNVPsurMOWCUeQA3VGyVGXPGtLMC+CdAM9jLpUyg6jw2xh0esxi+tYH7Uw==", - "dev": true, - "peer": true, - "requires": { - "@babel/traverse": "^7.14.0", - "@babel/types": "^7.0.0", - "invariant": "^2.2.4", - "metro-symbolicate": "0.70.3", - "nullthrows": "^1.1.1", - "ob1": "0.70.3", - "source-map": "^0.5.6", - "vlq": "^1.0.0" - }, - "dependencies": { - "source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", - "dev": true, - "peer": true - } - } - }, - "metro-symbolicate": { - "version": "0.70.3", - "resolved": "https://registry.npmjs.org/metro-symbolicate/-/metro-symbolicate-0.70.3.tgz", - "integrity": "sha512-JTYkF1dpeDUssQ84juE1ycnhHki2ylJBBdJE1JHtfu5oC+z1ElDbBdPHq90Uvt8HbRov/ZAnxvv7Zy6asS+WCA==", - "dev": true, - "peer": true, - "requires": { - "invariant": "^2.2.4", - "metro-source-map": "0.70.3", - "nullthrows": "^1.1.1", - "source-map": "^0.5.6", - "through2": "^2.0.1", - "vlq": "^1.0.0" - }, - "dependencies": { - "source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", - "dev": true, - "peer": true - } - } - }, - "metro-transform-plugins": { - "version": "0.70.3", - "resolved": "https://registry.npmjs.org/metro-transform-plugins/-/metro-transform-plugins-0.70.3.tgz", - "integrity": "sha512-dQRIJoTkWZN2IVS2KzgS1hs7ZdHDX3fS3esfifPkqFAEwHiLctCf0EsPgIknp0AjMLvmGWfSLJigdRB/dc0ASw==", - "dev": true, - "peer": true, - "requires": { - "@babel/core": "^7.14.0", - "@babel/generator": "^7.14.0", - "@babel/template": "^7.0.0", - "@babel/traverse": "^7.14.0", - "nullthrows": "^1.1.1" - } - }, - "metro-transform-worker": { - "version": "0.70.3", - "resolved": "https://registry.npmjs.org/metro-transform-worker/-/metro-transform-worker-0.70.3.tgz", - "integrity": "sha512-MtVVsnHhhBOp9GRLCdAb2mD1dTCsIzT4+m34KMRdBDCEbDIb90YafT5prpU8qbj5uKd0o2FOQdrJ5iy5zQilHw==", - "dev": true, - "peer": true, - "requires": { - "@babel/core": "^7.14.0", - "@babel/generator": "^7.14.0", - "@babel/parser": "^7.14.0", - "@babel/types": "^7.0.0", - "babel-preset-fbjs": "^3.4.0", - "metro": "0.70.3", - "metro-babel-transformer": "0.70.3", - "metro-cache": "0.70.3", - "metro-cache-key": "0.70.3", - "metro-hermes-compiler": "0.70.3", - "metro-source-map": "0.70.3", - "metro-transform-plugins": "0.70.3", - "nullthrows": "^1.1.1" - } - }, "micromatch": { "version": "4.0.5", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "dev": true, "requires": { "braces": "^3.0.2", "picomatch": "^2.3.1" @@ -26630,6 +14344,7 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, "requires": { "brace-expansion": "^1.1.7" } @@ -26637,28 +14352,8 @@ "minimist": { "version": "1.2.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==" - }, - "mixin-deep": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.2.tgz", - "integrity": "sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==", - "dev": true, - "peer": true, - "requires": { - "for-in": "^1.0.2", - "is-extendable": "^1.0.1" - } - }, - "mkdirp": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", - "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", - "dev": true, - "peer": true, - "requires": { - "minimist": "^1.2.6" - } + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true }, "mocha": { "version": "10.2.0", @@ -26746,22 +14441,22 @@ } }, "mocha-junit-reporter": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/mocha-junit-reporter/-/mocha-junit-reporter-2.2.0.tgz", - "integrity": "sha512-W83Ddf94nfLiTBl24aS8IVyFvO8aRDLlCvb+cKb/VEaN5dEbcqu3CXiTe8MQK2DvzS7oKE1RsFTxzN302GGbDQ==", + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/mocha-junit-reporter/-/mocha-junit-reporter-2.2.1.tgz", + "integrity": "sha512-iDn2tlKHn8Vh8o4nCzcUVW4q7iXp7cC4EB78N0cDHIobLymyHNwe0XG8HEHHjc3hJlXm0Vy6zcrxaIhnI2fWmw==", "dev": true, "requires": { "debug": "^4.3.4", "md5": "^2.3.0", - "mkdirp": "~1.0.4", + "mkdirp": "^3.0.0", "strip-ansi": "^6.0.1", "xml": "^1.0.1" }, "dependencies": { "mkdirp": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", - "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz", + "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==", "dev": true }, "strip-ansi": { @@ -27054,38 +14749,12 @@ "integrity": "sha512-p1sjXuopFs0xg+fPASzQ28agW1oHD7xDsd9Xkf3T15H3c/cifrFHVwrh74PdoklAPi+i7MdRsE47vm2r6JoB+w==", "dev": true }, - "nanomatch": { - "version": "1.2.13", - "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz", - "integrity": "sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==", - "dev": true, - "peer": true, - "requires": { - "arr-diff": "^4.0.0", - "array-unique": "^0.3.2", - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "fragment-cache": "^0.2.1", - "is-windows": "^1.0.2", - "kind-of": "^6.0.2", - "object.pick": "^1.3.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - } - }, "natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "dev": true }, - "natural-compare-lite": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz", - "integrity": "sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==", - "dev": true - }, "negotiator": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", @@ -27099,75 +14768,12 @@ "dev": true, "peer": true }, - "nice-try": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", - "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", - "dev": true, - "peer": true - }, - "nocache": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/nocache/-/nocache-3.0.4.tgz", - "integrity": "sha512-WDD0bdg9mbq6F4mRxEYcPWwfA1vxd0mrvKOyxI7Xj/atfRHVeutzuWByG//jfm4uPzp0y4Kj051EORCBSQMycw==", - "dev": true, - "peer": true - }, - "node-dir": { - "version": "0.1.17", - "resolved": "https://registry.npmjs.org/node-dir/-/node-dir-0.1.17.tgz", - "integrity": "sha512-tmPX422rYgofd4epzrNoOXiE8XFZYOcCq1vD7MAXCDO+O+zndlA2ztdKKMa+EeuBG5tHETpr4ml4RGgpqDCCAg==", - "dev": true, - "peer": true, - "requires": { - "minimatch": "^3.0.2" - } - }, "node-ensure": { "version": "0.0.0", "resolved": "https://registry.npmjs.org/node-ensure/-/node-ensure-0.0.0.tgz", "integrity": "sha1-7K52QVDemYYexcgQ/V0Jaxg5Mqc=", "dev": true }, - "node-fetch": { - "version": "2.6.7", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", - "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", - "dev": true, - "peer": true, - "requires": { - "whatwg-url": "^5.0.0" - } - }, - "node-int64": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", - "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", - "dev": true, - "peer": true - }, - "node-localstorage": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/node-localstorage/-/node-localstorage-1.3.1.tgz", - "integrity": "sha512-NMWCSWWc6JbHT5PyWlNT2i8r7PgGYXVntmKawY83k/M0UJScZ5jirb61TLnqKwd815DfBQu+lR3sRw08SPzIaQ==", - "dev": true, - "requires": { - "write-file-atomic": "^1.1.4" - }, - "dependencies": { - "write-file-atomic": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-1.3.4.tgz", - "integrity": "sha512-SdrHoC/yVBPpV0Xq/mUZQIpW2sWXAShb/V4pomcJXh92RuaO+f3UTWItiR3Px+pLnV2PvC2/bfn5cwr5X6Vfxw==", - "dev": true, - "requires": { - "graceful-fs": "^4.1.11", - "imurmurhash": "^0.1.4", - "slide": "^1.1.5" - } - } - } - }, "node-releases": { "version": "2.0.10", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.10.tgz", @@ -27175,13 +14781,6 @@ "dev": true, "peer": true }, - "node-stream-zip": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/node-stream-zip/-/node-stream-zip-1.15.0.tgz", - "integrity": "sha512-LN4fydt9TqhZhThkZIVQnF9cwjU3qmUH9h78Mx/K7d3VvfRqqwthLwJEUOEL0QPZ0XQmNN7be5Ggit5+4dq3Bw==", - "dev": true, - "peer": true - }, "normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -27203,101 +14802,12 @@ "path-key": "^3.0.0" } }, - "nullthrows": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/nullthrows/-/nullthrows-1.1.1.tgz", - "integrity": "sha512-2vPPEi+Z7WqML2jZYddDIfy5Dqb0r2fze2zTxNNknZaFpVHU3mFB3R+DWeJWGVx0ecvttSGlJTI+WG+8Z4cDWw==", - "dev": true, - "peer": true - }, - "ob1": { - "version": "0.70.3", - "resolved": "https://registry.npmjs.org/ob1/-/ob1-0.70.3.tgz", - "integrity": "sha512-Vy9GGhuXgDRY01QA6kdhToPd8AkLdLpX9GjH5kpqluVqTu70mgOm7tpGoJDZGaNbr9nJlJgnipqHJQRPORixIQ==", - "dev": true, - "peer": true - }, "object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", "dev": true }, - "object-copy": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz", - "integrity": "sha512-79LYn6VAb63zgtmAteVOWo9Vdj71ZVBy3Pbse+VqxDpEP83XuujMrGqHIwAXJ5I/aM0zU7dIyIAhifVTPrNItQ==", - "dev": true, - "peer": true, - "requires": { - "copy-descriptor": "^0.1.0", - "define-property": "^0.2.5", - "kind-of": "^3.0.3" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA==", - "dev": true, - "peer": true, - "requires": { - "is-descriptor": "^0.1.0" - } - }, - "is-accessor-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", - "integrity": "sha512-e1BM1qnDbMRG3ll2U9dSK0UMHuWOs3pY3AtcFsmvwPtKL3MML/Q86i+GilLfvqEs4GW+ExB91tQ3Ig9noDIZ+A==", - "dev": true, - "peer": true, - "requires": { - "kind-of": "^3.0.2" - } - }, - "is-data-descriptor": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", - "integrity": "sha512-+w9D5ulSoBNlmw9OHn3U2v51SyoCd0he+bB3xMl62oijhrspxowjU+AIcDY0N3iEJbUEkB15IlMASQsxYigvXg==", - "dev": true, - "peer": true, - "requires": { - "kind-of": "^3.0.2" - } - }, - "is-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", - "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", - "dev": true, - "peer": true, - "requires": { - "is-accessor-descriptor": "^0.1.6", - "is-data-descriptor": "^0.1.4", - "kind-of": "^5.0.0" - }, - "dependencies": { - "kind-of": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", - "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", - "dev": true, - "peer": true - } - } - }, - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", - "dev": true, - "peer": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, "object-inspect": { "version": "1.12.3", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz", @@ -27320,16 +14830,6 @@ "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", "dev": true }, - "object-visit": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz", - "integrity": "sha512-GBaMwwAVK9qbQN3Scdo0OyvgPW7l3lnaVMj84uTOZlswkX0KpF6fyDBJhtTthf7pymztoN36/KEr1DyhF96zEA==", - "dev": true, - "peer": true, - "requires": { - "isobject": "^3.0.0" - } - }, "object.assign": { "version": "4.1.4", "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.4.tgz", @@ -27364,6 +14864,18 @@ "es-abstract": "^1.20.4" } }, + "object.groupby": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.1.tgz", + "integrity": "sha512-HqaQtqLnp/8Bn4GL16cj+CUYbnpe1bh0TtEaWvybszDG4tgxCJuRpV8VGuvNaI1fAnI4lUJzDG55MXcOH4JZcQ==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "get-intrinsic": "^1.2.1" + } + }, "object.hasown": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/object.hasown/-/object.hasown-1.1.2.tgz", @@ -27374,16 +14886,6 @@ "es-abstract": "^1.20.4" } }, - "object.pick": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz", - "integrity": "sha512-tqa/UMy/CCoYmj+H5qc07qvSL9dqcs/WZENZ1JbtWBlATP+iVOe778gE6MSijnyCnORzDuX6hU+LA4SZ09YjFQ==", - "dev": true, - "peer": true, - "requires": { - "isobject": "^3.0.1" - } - }, "object.values": { "version": "1.1.6", "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.6.tgz", @@ -27395,27 +14897,11 @@ "es-abstract": "^1.20.4" } }, - "on-finished": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", - "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==", - "dev": true, - "peer": true, - "requires": { - "ee-first": "1.1.1" - } - }, - "on-headers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", - "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", - "dev": true, - "peer": true - }, "once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dev": true, "requires": { "wrappy": "1" } @@ -27429,16 +14915,6 @@ "mimic-fn": "^2.1.0" } }, - "open": { - "version": "6.4.0", - "resolved": "https://registry.npmjs.org/open/-/open-6.4.0.tgz", - "integrity": "sha512-IFenVPgF70fSm1keSd2iDBIDIBZkroLeuffXq+wKTzTJlBpesFWojV9lb8mzOfaAzM1sr7HQHuO0vtV0zYekGg==", - "dev": true, - "peer": true, - "requires": { - "is-wsl": "^1.1.0" - } - }, "opener": { "version": "1.5.2", "resolved": "https://registry.npmjs.org/opener/-/opener-1.5.2.tgz", @@ -27446,67 +14922,25 @@ "dev": true }, "optionator": { - "version": "0.9.1", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", - "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==", + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", + "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==", "dev": true, "requires": { + "@aashutoshrathi/word-wrap": "^1.2.3", "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.3" + "type-check": "^0.4.0" } }, - "ora": { - "version": "5.4.1", - "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", - "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", - "dev": true, - "peer": true, - "requires": { - "bl": "^4.1.0", - "chalk": "^4.1.0", - "cli-cursor": "^3.1.0", - "cli-spinners": "^2.5.0", - "is-interactive": "^1.0.0", - "is-unicode-supported": "^0.1.0", - "log-symbols": "^4.1.0", - "strip-ansi": "^6.0.0", - "wcwidth": "^1.0.1" - }, - "dependencies": { - "strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "peer": true, - "requires": { - "ansi-regex": "^5.0.1" - } - } - } - }, - "os-tmpdir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", - "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==" - }, "ospath": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/ospath/-/ospath-1.2.2.tgz", "integrity": "sha1-EnZjl3Sj+O8lcvf+QoDg6kVQwHs=", "dev": true }, - "p-finally": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", - "integrity": "sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==", - "dev": true, - "peer": true - }, "p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -27563,88 +14997,12 @@ } } }, - "parse-json": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", - "integrity": "sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw==", - "dev": true, - "peer": true, - "requires": { - "error-ex": "^1.3.1", - "json-parse-better-errors": "^1.0.1" - } - }, "parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", "dev": true }, - "pascalcase": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz", - "integrity": "sha512-XHXfu/yOQRy9vYOtUDVMN60OEJjW013GoObG1o+xwQTpB9eYJX/BjXMsdW13ZDPruFhYYn0AG22w0xgQMwl3Nw==", - "dev": true, - "peer": true - }, - "patch-package": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/patch-package/-/patch-package-7.0.0.tgz", - "integrity": "sha512-eYunHbnnB2ghjTNc5iL1Uo7TsGMuXk0vibX3RFcE/CdVdXzmdbMsG/4K4IgoSuIkLTI5oHrMQk4+NkFqSed0BQ==", - "requires": { - "@yarnpkg/lockfile": "^1.1.0", - "chalk": "^4.1.2", - "ci-info": "^3.7.0", - "cross-spawn": "^7.0.3", - "find-yarn-workspace-root": "^2.0.0", - "fs-extra": "^9.0.0", - "klaw-sync": "^6.0.0", - "minimist": "^1.2.6", - "open": "^7.4.2", - "rimraf": "^2.6.3", - "semver": "^5.6.0", - "slash": "^2.0.0", - "tmp": "^0.0.33", - "yaml": "^2.2.2" - }, - "dependencies": { - "is-wsl": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", - "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", - "requires": { - "is-docker": "^2.0.0" - } - }, - "open": { - "version": "7.4.2", - "resolved": "https://registry.npmjs.org/open/-/open-7.4.2.tgz", - "integrity": "sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==", - "requires": { - "is-docker": "^2.0.0", - "is-wsl": "^2.1.1" - } - }, - "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" - }, - "slash": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz", - "integrity": "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==" - }, - "tmp": { - "version": "0.0.33", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", - "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", - "requires": { - "os-tmpdir": "~1.0.2" - } - } - } - }, "path": { "version": "0.12.7", "resolved": "https://registry.npmjs.org/path/-/path-0.12.7.tgz", @@ -27664,12 +15022,14 @@ "path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "dev": true }, "path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==" + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true }, "path-parse": { "version": "1.0.7", @@ -27729,24 +15089,40 @@ "dev": true }, "pg": { - "version": "8.10.0", - "resolved": "https://registry.npmjs.org/pg/-/pg-8.10.0.tgz", - "integrity": "sha512-ke7o7qSTMb47iwzOSaZMfeR7xToFdkE71ifIipOAAaLIM0DYzfOAXlgFFmYUIE2BcJtvnVlGCID84ZzCegE8CQ==", + "version": "8.11.3", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.11.3.tgz", + "integrity": "sha512-+9iuvG8QfaaUrrph+kpF24cXkH1YOOUeArRNYIxq1viYHZagBxrTno7cecY1Fa44tJeZvaoG+Djpkc3JwehN5g==", "dev": true, "requires": { "buffer-writer": "2.0.0", "packet-reader": "1.0.0", - "pg-connection-string": "^2.5.0", - "pg-pool": "^3.6.0", + "pg-cloudflare": "^1.1.1", + "pg-connection-string": "^2.6.2", + "pg-pool": "^3.6.1", "pg-protocol": "^1.6.0", "pg-types": "^2.1.0", "pgpass": "1.x" + }, + "dependencies": { + "pg-connection-string": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.6.2.tgz", + "integrity": "sha512-ch6OwaeaPYcova4kKZ15sbJ2hKb/VP48ZD2gE7i1J+L4MspCtBMAx8nMgz7bksc7IojCIIWuEhHibSMFH8m8oA==", + "dev": true + } } }, + "pg-cloudflare": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.1.1.tgz", + "integrity": "sha512-xWPagP/4B6BgFO+EKz3JONXv3YDgvkbVrGw2mTo3D6tVDQRh1e7cqVGvyR3BE+eQgAvx1XhW/iEASj4/jCWl3Q==", + "dev": true, + "optional": true + }, "pg-connection-string": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.5.0.tgz", - "integrity": "sha512-r5o/V/ORTA6TmUnyWZR9nCj1klXCO2CEKNRlVuJptZe85QuhFayC7WeMic7ndayT5IRIR0S0xFxFi2ousartlQ==", + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.6.1.tgz", + "integrity": "sha512-w6ZzNu6oMmIzEAYVw+RLK0+nqHPt8K3ZnknKi+g48Ak2pr3dtljJW3o+D/n2zzCG07Zoe9VOX3aiKpj+BN0pjg==", "dev": true }, "pg-int8": { @@ -27756,9 +15132,9 @@ "dev": true }, "pg-pool": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.6.0.tgz", - "integrity": "sha512-clFRf2ksqd+F497kWFyM21tMjeikn60oGDmqMT8UBrynEwVEX/5R5xd2sdvdo1cZCFlguORNpVuqxIj+aK4cfQ==", + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.6.1.tgz", + "integrity": "sha512-jizsIzhkIitxCGfPRzJn1ZdcosIt3pz9Sh3V01fm1vZnbnCMgmGl5wvGGdNN2EL9Rmb0EcFoCkixH4Pu+sP9Og==", "dev": true, "requires": {} }, @@ -27800,7 +15176,8 @@ "picomatch": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==" + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true }, "pify": { "version": "2.3.0", @@ -27808,99 +15185,12 @@ "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", "dev": true }, - "pirates": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.5.tgz", - "integrity": "sha512-8V9+HQPupnaXMA23c5hvl69zXvTwTzyAYasnkb0Tts4XvO4CliqONMOnvlq26rkhLC3nWDFBJf73LU1e1VZLaQ==", - "dev": true, - "peer": true - }, - "pkg-dir": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-3.0.0.tgz", - "integrity": "sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw==", - "dev": true, - "peer": true, - "requires": { - "find-up": "^3.0.0" - }, - "dependencies": { - "find-up": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", - "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", - "dev": true, - "peer": true, - "requires": { - "locate-path": "^3.0.0" - } - }, - "locate-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", - "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", - "dev": true, - "peer": true, - "requires": { - "p-locate": "^3.0.0", - "path-exists": "^3.0.0" - } - }, - "p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dev": true, - "peer": true, - "requires": { - "p-try": "^2.0.0" - } - }, - "p-locate": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", - "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", - "dev": true, - "peer": true, - "requires": { - "p-limit": "^2.0.0" - } - }, - "path-exists": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", - "integrity": "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==", - "dev": true, - "peer": true - } - } - }, "platform": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/platform/-/platform-1.3.3.tgz", "integrity": "sha1-ZGx3ARiZhwtqCQPnXpl+jlHadGE=", "dev": true }, - "plist": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/plist/-/plist-3.0.6.tgz", - "integrity": "sha512-WiIVYyrp8TD4w8yCvyeIr+lkmrGRd5u0VbRnU+tP/aRLxP/YadJUYOMZJ/6hIa3oUyVCsycXvtNRgd5XBJIbiA==", - "dev": true, - "peer": true, - "requires": { - "base64-js": "^1.5.1", - "xmlbuilder": "^15.1.1" - }, - "dependencies": { - "xmlbuilder": { - "version": "15.1.1", - "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.1.1.tgz", - "integrity": "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==", - "dev": true, - "peer": true - } - } - }, "popsicle": { "version": "12.1.0", "resolved": "https://registry.npmjs.org/popsicle/-/popsicle-12.1.0.tgz", @@ -27977,13 +15267,6 @@ "dev": true, "requires": {} }, - "posix-character-classes": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz", - "integrity": "sha512-xTgYBc3fuo7Yt7JbiuFxSYGToMoz8fLoE6TC9Wx1P/u+LfeThMOAqmuyECnlBaaJb+u1m9hHiXUEtwW4OzfUJg==", - "dev": true, - "peer": true - }, "postgres-array": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", @@ -28023,45 +15306,6 @@ "integrity": "sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==", "dev": true }, - "pretty-format": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-26.6.2.tgz", - "integrity": "sha512-7AeGuCYNGmycyQbCqd/3PWH4eOoX/OiCa0uphp57NVTeAGdJGaAliecxwBDHYQCIvrW7aDBZCYeNTP/WX69mkg==", - "dev": true, - "peer": true, - "requires": { - "@jest/types": "^26.6.2", - "ansi-regex": "^5.0.0", - "ansi-styles": "^4.0.0", - "react-is": "^17.0.1" - }, - "dependencies": { - "@jest/types": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-26.6.2.tgz", - "integrity": "sha512-fC6QCp7Sc5sX6g8Tvbmj4XUTbyrik0akgRy03yjXbQaBWWNWGE7SGtJk98m0N8nzegD/7SggrUlivxo5ax4KWQ==", - "dev": true, - "peer": true, - "requires": { - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^15.0.0", - "chalk": "^4.0.0" - } - }, - "@types/yargs": { - "version": "15.0.14", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.14.tgz", - "integrity": "sha512-yEJzHoxf6SyQGhBhIYGXQDSCkJjB6HohDShto7m8vaKg9Yp0Yn8+71J9eakh2bnPg6BfsH9PRMhiRTZnd4eXGQ==", - "dev": true, - "peer": true, - "requires": { - "@types/yargs-parser": "*" - } - } - } - }, "process": { "version": "0.11.10", "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", @@ -28074,27 +15318,6 @@ "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", "dev": true }, - "promise": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/promise/-/promise-8.1.0.tgz", - "integrity": "sha512-W04AqnILOL/sPRXziNicCjSNRruLAuIHEOVBazepu0545DDNGYHz7ar9ZgZ1fMU8/MA4mVxp5rkBWRi6OXIy3Q==", - "dev": true, - "peer": true, - "requires": { - "asap": "~2.0.6" - } - }, - "prompts": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", - "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", - "dev": true, - "peer": true, - "requires": { - "kleur": "^3.0.3", - "sisteransi": "^1.0.5" - } - }, "prop-types": { "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", @@ -28173,6 +15396,12 @@ "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=", "dev": true }, + "querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", + "dev": true + }, "queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -28206,123 +15435,12 @@ "unpipe": "1.0.0" } }, - "react": { - "version": "18.0.0", - "resolved": "https://registry.npmjs.org/react/-/react-18.0.0.tgz", - "integrity": "sha512-x+VL6wbT4JRVPm7EGxXhZ8w8LTROaxPXOqhlGyVSrv0sB1jkyFGgXxJ8LVoPRLvPR6/CIZGFmfzqUa2NYeMr2A==", - "dev": true, - "peer": true, - "requires": { - "loose-envify": "^1.1.0" - } - }, - "react-devtools-core": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/react-devtools-core/-/react-devtools-core-4.24.0.tgz", - "integrity": "sha512-Rw7FzYOOzcfyUPaAm9P3g0tFdGqGq2LLiAI+wjYcp6CsF3DeeMrRS3HZAho4s273C29G/DJhx0e8BpRE/QZNGg==", - "dev": true, - "peer": true, - "requires": { - "shell-quote": "^1.6.1", - "ws": "^7" - }, - "dependencies": { - "ws": { - "version": "7.5.8", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.8.tgz", - "integrity": "sha512-ri1Id1WinAX5Jqn9HejiGb8crfRio0Qgu8+MtL36rlTA6RLsMdWt1Az/19A2Qij6uSHUMphEFaTKa4WG+UNHNw==", - "dev": true, - "peer": true, - "requires": {} - } - } - }, "react-is": { "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", "dev": true }, - "react-native": { - "version": "0.69.1", - "resolved": "https://registry.npmjs.org/react-native/-/react-native-0.69.1.tgz", - "integrity": "sha512-585NmzSuYUfday8YsfqgreFAZbXRI/kxKsiuaShwGHgkwdtmE5qA7WfGItgxZBOZD6g/Hm1YBUqSwIm07tPa6A==", - "dev": true, - "peer": true, - "requires": { - "@jest/create-cache-key-function": "^27.0.1", - "@react-native-community/cli": "^8.0.0", - "@react-native-community/cli-platform-android": "^8.0.0", - "@react-native-community/cli-platform-ios": "^8.0.0", - "@react-native/assets": "1.0.0", - "@react-native/normalize-color": "2.0.0", - "@react-native/polyfills": "2.0.0", - "abort-controller": "^3.0.0", - "anser": "^1.4.9", - "base64-js": "^1.1.2", - "event-target-shim": "^5.0.1", - "hermes-engine": "~0.11.0", - "invariant": "^2.2.4", - "jsc-android": "^250230.2.1", - "memoize-one": "^5.0.0", - "metro-react-native-babel-transformer": "0.70.3", - "metro-runtime": "0.70.3", - "metro-source-map": "0.70.3", - "mkdirp": "^0.5.1", - "nullthrows": "^1.1.1", - "pretty-format": "^26.5.2", - "promise": "^8.0.3", - "react-devtools-core": "4.24.0", - "react-native-codegen": "^0.69.1", - "react-native-gradle-plugin": "^0.0.7", - "react-refresh": "^0.4.0", - "react-shallow-renderer": "16.14.1", - "regenerator-runtime": "^0.13.2", - "scheduler": "^0.21.0", - "stacktrace-parser": "^0.1.3", - "use-sync-external-store": "^1.0.0", - "whatwg-fetch": "^3.0.0", - "ws": "^6.1.4" - } - }, - "react-native-codegen": { - "version": "0.69.1", - "resolved": "https://registry.npmjs.org/react-native-codegen/-/react-native-codegen-0.69.1.tgz", - "integrity": "sha512-TOZEqBarczcyYN3iZE3VpKkooOevaAzBq9n7lU0h9mQUvtRhLVyolc+a5K6cWI0e4v4K69I0MqzjPcPeFKo32Q==", - "dev": true, - "peer": true, - "requires": { - "@babel/parser": "^7.14.0", - "flow-parser": "^0.121.0", - "jscodeshift": "^0.13.1", - "nullthrows": "^1.1.1" - } - }, - "react-native-gradle-plugin": { - "version": "0.0.7", - "resolved": "https://registry.npmjs.org/react-native-gradle-plugin/-/react-native-gradle-plugin-0.0.7.tgz", - "integrity": "sha512-+4JpbIx42zGTONhBTIXSyfyHICHC29VTvhkkoUOJAh/XHPEixpuBduYgf6Y4y9wsN1ARlQhBBoptTvXvAFQf5g==", - "dev": true, - "peer": true - }, - "react-refresh": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.4.3.tgz", - "integrity": "sha512-Hwln1VNuGl/6bVwnd0Xdn1e84gT/8T9aYNL+HAKDArLCS7LWjwr7StE30IEYbIkx0Vi3vs+coQxe+SQDbGbbpA==", - "dev": true, - "peer": true - }, - "react-shallow-renderer": { - "version": "16.14.1", - "resolved": "https://registry.npmjs.org/react-shallow-renderer/-/react-shallow-renderer-16.14.1.tgz", - "integrity": "sha512-rkIMcQi01/+kxiTE9D3fdS959U1g7gs+/rborw++42m1O9FAQiNI/UNRZExVUoAOprn4umcXf+pFRou8i4zuBg==", - "dev": true, - "peer": true, - "requires": { - "object-assign": "^4.1.1", - "react-is": "^16.12.0 || ^17.0.0" - } - }, "readable-stream": { "version": "2.3.7", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", @@ -28355,35 +15473,6 @@ "picomatch": "^2.2.1" } }, - "readline": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/readline/-/readline-1.3.0.tgz", - "integrity": "sha512-k2d6ACCkiNYz222Fs/iNze30rRJ1iIicW7JuX/7/cozvih6YCkFZH+J6mAFDVgv0dRBaAyr4jDqC95R2y4IADg==", - "dev": true, - "peer": true - }, - "recast": { - "version": "0.20.5", - "resolved": "https://registry.npmjs.org/recast/-/recast-0.20.5.tgz", - "integrity": "sha512-E5qICoPoNL4yU0H0NoBDntNB0Q5oMSNh9usFctYniLBluTthi3RsQVBXIJNbApOlvSwW/RGxIuokPcAc59J5fQ==", - "dev": true, - "peer": true, - "requires": { - "ast-types": "0.14.2", - "esprima": "~4.0.0", - "source-map": "~0.6.1", - "tslib": "^2.0.1" - }, - "dependencies": { - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "peer": true - } - } - }, "rechoir": { "version": "0.6.2", "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz", @@ -28402,104 +15491,18 @@ "minimatch": "^3.0.5" } }, - "redux": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/redux/-/redux-4.0.5.tgz", - "integrity": "sha512-VSz1uMAH24DM6MF72vcojpYPtrTUu3ByVWfPL1nPfVRb5mZVTve5GnNCUV53QM/BZ66xfWrm0CTWoM+Xlz8V1w==", + "reflect.getprototypeof": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.4.tgz", + "integrity": "sha512-ECkTw8TmJwW60lOTR+ZkODISW6RQ8+2CL3COqtiJKLd6MmB45hN51HprHFziKLGkAuTGQhBb91V8cy+KHlaCjw==", "dev": true, "requires": { - "loose-envify": "^1.4.0", - "symbol-observable": "^1.2.0" - } - }, - "redux-action-buffer": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/redux-action-buffer/-/redux-action-buffer-1.2.0.tgz", - "integrity": "sha512-SvXSJQrn1Nsmza+xVMlvqZf0eiHIPV3I796jVC2DCC8X8+JXpAaSFuNxlxV0Cn+TAilat7KZ3srTwG5+HHL8tw==", - "dev": true - }, - "redux-devtools-core": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/redux-devtools-core/-/redux-devtools-core-0.2.1.tgz", - "integrity": "sha512-RAGOxtUFdr/1USAvxrWd+Gq/Euzgw7quCZlO5TgFpDfG7rB5tMhZUrNyBjpzgzL2yMk0eHnPYIGm7NkIfRzHxQ==", - "dev": true, - "requires": { - "get-params": "^0.1.2", - "jsan": "^3.1.13", - "lodash": "^4.17.11", - "nanoid": "^2.0.0", - "remotedev-serialize": "^0.1.8" - }, - "dependencies": { - "nanoid": { - "version": "2.1.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-2.1.11.tgz", - "integrity": "sha512-s/snB+WGm6uwi0WjsZdaVcuf3KJXlfGl2LcxgwkEwJF0D/BWzVWAZW/XY4bFaiR7s0Jk3FPvlnepg1H1b1UwlA==", - "dev": true - } - } - }, - "redux-devtools-instrument": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/redux-devtools-instrument/-/redux-devtools-instrument-1.10.0.tgz", - "integrity": "sha512-X8JRBCzX2ADSMp+iiV7YQ8uoTNyEm0VPFPd4T854coz6lvRiBrFSqAr9YAS2n8Kzxx8CJQotR0QF9wsMM+3DvA==", - "dev": true, - "requires": { - "lodash": "^4.17.19", - "symbol-observable": "^1.2.0" - } - }, - "redux-offline": { - "version": "git+ssh://git@github.com/enahum/redux-offline.git#885024de96b6ec73650c340c8928066585c413df", - "integrity": "sha512-srmJ1vWm8ZQTYflZCf7oUs3WBX83GyCIzsFUpwxUg2wcDHngSHjjShRTCgmkciPkVmM4aJ33i9baYS9jRC+zLA==", - "dev": true, - "from": "redux-offline@git+https://github.com/enahum/redux-offline.git#885024de96b6ec73650c340c8928066585c413df", - "requires": { - "@react-native-community/netinfo": "^4.1.3", - "redux-persist": "^4.5.0" - } - }, - "redux-persist": { - "version": "4.9.1", - "resolved": "https://registry.npmjs.org/redux-persist/-/redux-persist-4.9.1.tgz", - "integrity": "sha512-XoOmfPyo9GEU/WLH9FgB47dNIN9l5ArjHes4o7vUWx9nxZoPxnVodhuHdyc4Ot+fMkdj3L2LTqSHhwrkr0QFUg==", - "dev": true, - "requires": { - "json-stringify-safe": "^5.0.1", - "lodash": "^4.17.4", - "lodash-es": "^4.17.4" - } - }, - "redux-persist-node-storage": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/redux-persist-node-storage/-/redux-persist-node-storage-2.0.0.tgz", - "integrity": "sha512-nytPz/iNTrAO4o8A17UaipPl8tVcrnm84r6v0tgHJ+q0ysEzyS/rBlPGXrKNIPplXi/W4riUTQlCIhajodIfJg==", - "dev": true, - "requires": { - "node-localstorage": "^1.3.0" - } - }, - "redux-thunk": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-2.3.0.tgz", - "integrity": "sha512-km6dclyFnmcvxhAcrQV2AkZmPQjzPDjgVlQtR0EQjxZPyJ0BnMf3in1ryuR8A2qU0HldVRfxYXbFSKlI3N7Slw==", - "dev": true - }, - "regenerate": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", - "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==", - "dev": true, - "peer": true - }, - "regenerate-unicode-properties": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.1.0.tgz", - "integrity": "sha512-d1VudCLoIGitcU/hEg2QqvyGZQmdC0Lf8BqdOMXGFSvJP4bNV1+XqbPQeHHLD51Jh4QJJ225dlIFvY4Ly6MXmQ==", - "dev": true, - "peer": true, - "requires": { - "regenerate": "^1.4.2" + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "get-intrinsic": "^1.2.1", + "globalthis": "^1.0.3", + "which-builtin-type": "^1.1.3" } }, "regenerator-runtime": { @@ -28508,109 +15511,17 @@ "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==", "dev": true }, - "regenerator-transform": { - "version": "0.15.0", - "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.15.0.tgz", - "integrity": "sha512-LsrGtPmbYg19bcPHwdtmXwbW+TqNvtY4riE3P83foeHRroMbH6/2ddFBfab3t7kbzc7v7p4wbkIecHImqt0QNg==", - "dev": true, - "peer": true, - "requires": { - "@babel/runtime": "^7.8.4" - } - }, - "regex-not": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz", - "integrity": "sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==", - "dev": true, - "peer": true, - "requires": { - "extend-shallow": "^3.0.2", - "safe-regex": "^1.1.0" - } - }, "regexp.prototype.flags": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.4.3.tgz", - "integrity": "sha512-fjggEOO3slI6Wvgjwflkc4NFRCTZAu5CnNfBd5qOMYhWdn67nJBBu34/TkD++eeFmd8C9r9jfXJ27+nSiRkSUA==", + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.1.tgz", + "integrity": "sha512-sy6TXMN+hnP/wMy+ISxg3krXx7BAtWVO4UouuCN/ziM9UEne0euamVNafDfvC83bRNr95y0V5iijeDQFUNpvrg==", "dev": true, "requires": { "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "functions-have-names": "^1.2.2" + "define-properties": "^1.2.0", + "set-function-name": "^2.0.0" } }, - "regexpu-core": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-5.3.2.tgz", - "integrity": "sha512-RAM5FlZz+Lhmo7db9L298p2vHP5ZywrVXmVXpmAD9GuL5MPH6t9ROw1iA/wfHkQ76Qe7AaPF0nGuim96/IrQMQ==", - "dev": true, - "peer": true, - "requires": { - "@babel/regjsgen": "^0.8.0", - "regenerate": "^1.4.2", - "regenerate-unicode-properties": "^10.1.0", - "regjsparser": "^0.9.1", - "unicode-match-property-ecmascript": "^2.0.0", - "unicode-match-property-value-ecmascript": "^2.1.0" - } - }, - "regjsparser": { - "version": "0.9.1", - "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.9.1.tgz", - "integrity": "sha512-dQUtn90WanSNl+7mQKcXAgZxvUe7Z0SqXlgzv0za4LwiUhyzBC58yQO3liFoUgu8GiJVInAhJjkj1N0EtQ5nkQ==", - "dev": true, - "peer": true, - "requires": { - "jsesc": "~0.5.0" - }, - "dependencies": { - "jsesc": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", - "integrity": "sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==", - "dev": true, - "peer": true - } - } - }, - "remote-redux-devtools": { - "version": "0.5.16", - "resolved": "https://registry.npmjs.org/remote-redux-devtools/-/remote-redux-devtools-0.5.16.tgz", - "integrity": "sha512-xZ2D1VRIWzat5nsvcraT6fKEX9Cfi+HbQBCwzNnUAM8Uicm/anOc60XGalcaDPrVmLug7nhDl2nimEa3bL3K9w==", - "dev": true, - "requires": { - "jsan": "^3.1.13", - "querystring": "^0.2.0", - "redux-devtools-core": "^0.2.1", - "redux-devtools-instrument": "^1.9.4", - "rn-host-detect": "^1.1.5", - "socketcluster-client": "^14.2.1" - } - }, - "remotedev-serialize": { - "version": "0.1.9", - "resolved": "https://registry.npmjs.org/remotedev-serialize/-/remotedev-serialize-0.1.9.tgz", - "integrity": "sha512-5tFdZg9mSaAWTv6xmQ7HtHjKMLSFQFExEZOtJe10PLsv1wb7cy7kYHtBvTYRro27/3fRGEcQBRNKSaixOpb69w==", - "dev": true, - "requires": { - "jsan": "^3.1.13" - } - }, - "repeat-element": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.4.tgz", - "integrity": "sha512-LFiNfRcSu7KK3evMyYOuCzv3L10TW7yC1G2/+StMjK8Y6Vqd2MG7r/Qjw4ghtuCOjFvlnms/iMmLqpvW/ES/WQ==", - "dev": true, - "peer": true - }, - "repeat-string": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", - "integrity": "sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==", - "dev": true, - "peer": true - }, "request-progress": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/request-progress/-/request-progress-3.0.0.tgz", @@ -28632,10 +15543,10 @@ "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", "dev": true }, - "reselect": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/reselect/-/reselect-4.0.0.tgz", - "integrity": "sha512-qUgANli03jjAyGlnbYVAV5vvnOmJnODyABz51RdBN7M4WaVu8mecZWgyQNkG8Yqe3KRGRt0l4K4B3XVEULC4CA==", + "requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", "dev": true }, "resolve": { @@ -28648,20 +15559,6 @@ "path-parse": "^1.0.6" } }, - "resolve-from": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-3.0.0.tgz", - "integrity": "sha512-GnlH6vxLymXJNMBo7XP1fJIzBFbdYt49CuTwmB/6N53t+kMPRMFKz783LlQ4tv28XoQfMWinAJX6WCGf2IlaIw==", - "dev": true, - "peer": true - }, - "resolve-url": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", - "integrity": "sha512-ZuF55hVUQaaczgOIwqWzkEcEidmlD/xl44x1UZnhOXcYuFN2S6+rcxpG+C1N3So0wvNI3DmJICUFfu2SxhBmvg==", - "dev": true, - "peer": true - }, "restore-cursor": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", @@ -28672,39 +15569,12 @@ "signal-exit": "^3.0.2" } }, - "ret": { - "version": "0.1.15", - "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", - "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==", - "dev": true, - "peer": true - }, "reusify": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", "dev": true }, - "rimraf": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", - "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", - "requires": { - "glob": "^7.1.3" - } - }, - "rn-host-detect": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/rn-host-detect/-/rn-host-detect-1.2.0.tgz", - "integrity": "sha512-btNg5kzHcjZZ7t7mvvV/4wNJ9e3MPgrWivkRgWURzXL0JJ0pwWlU4zrbmdlz3HHzHOxhBhHB4D+/dbMFfu4/4A==", - "dev": true - }, - "rudder-sdk-js": { - "version": "1.0.14", - "resolved": "https://registry.npmjs.org/rudder-sdk-js/-/rudder-sdk-js-1.0.14.tgz", - "integrity": "sha512-q86qmF6VUXjTUCv1YRFc2V1pt408D641nD2ymKFaLtGZTLjkZCzotQ7cW8c48vPKlSWwekkBlA3kghy/NKGwOw==", - "dev": true - }, "run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -28731,22 +15601,32 @@ } } }, + "safe-array-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.0.1.tgz", + "integrity": "sha512-6XbUAseYE2KtOuGueyeobCySj9L4+66Tn6KQMOPQJrAJEowYKW/YR/MGJZl7FdydUdaFu4LYyDZjxf4/Nmo23Q==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.2.1", + "has-symbols": "^1.0.3", + "isarray": "^2.0.5" + }, + "dependencies": { + "isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true + } + } + }, "safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", "dev": true }, - "safe-regex": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", - "integrity": "sha512-aJXcif4xnaNUzvUuC5gcb46oTS7zvg4jpMTnuqtrEPlR3vFr4pxtdTwaF1Qs3Enjn9HK+ZlwQui+a7z0SywIzg==", - "dev": true, - "peer": true, - "requires": { - "ret": "~0.1.10" - } - }, "safe-regex-test": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.0.tgz", @@ -28770,37 +15650,6 @@ "integrity": "sha512-8I2a3LovHTOpm7NV5yOyO8IHqgVsfK4+UuySrXU8YXkSRX7k6hCV9b3HrkKCr3nMpgj+0bmocaJJWpvp1oc7ZA==", "dev": true }, - "sc-channel": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/sc-channel/-/sc-channel-1.2.0.tgz", - "integrity": "sha512-M3gdq8PlKg0zWJSisWqAsMmTVxYRTpVRqw4CWAdKBgAfVKumFcTjoCV0hYu7lgUXccCtCD8Wk9VkkE+IXCxmZA==", - "dev": true, - "requires": { - "component-emitter": "1.2.1" - } - }, - "sc-errors": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/sc-errors/-/sc-errors-2.0.1.tgz", - "integrity": "sha512-JoVhq3Ud+3Ujv2SIG7W0XtjRHsrNgl6iXuHHsh0s+Kdt5NwI6N2EGAZD4iteitdDv68ENBkpjtSvN597/wxPSQ==", - "dev": true - }, - "sc-formatter": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/sc-formatter/-/sc-formatter-3.0.3.tgz", - "integrity": "sha512-lYI/lTs1u1c0geKElcj+bmEUfcP/HuKg2iDeTijPSjiTNFzN3Cf8Qh6tVd65oi7Qn+2/oD7LP4s6GC13v/9NiQ==", - "dev": true - }, - "scheduler": { - "version": "0.21.0", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.21.0.tgz", - "integrity": "sha512-1r87x5fz9MXqswA2ERLo0EbOAU74DpIUO090gIasYTqlVoJeMcl+Z1Rg7WHz+qtPujhS/hGIt9kxZOYBV3faRQ==", - "dev": true, - "peer": true, - "requires": { - "loose-envify": "^1.1.0" - } - }, "schema-utils": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.1.tgz", @@ -28886,23 +15735,6 @@ } } }, - "serialize-error": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-6.0.0.tgz", - "integrity": "sha512-3vmBkMZLQO+BR4RPHcyRGdE09XCF6cvxzk2N2qn8Er3F91cy8Qt7VvEbZBOpaL53qsBbe2cFOefU6tRY6WDelA==", - "dev": true, - "requires": { - "type-fest": "^0.12.0" - }, - "dependencies": { - "type-fest": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.12.0.tgz", - "integrity": "sha512-53RyidyjvkGpnWPMF9bQgFtWp+Sl8O2Rp13VavmJgfAP9WWG6q6TkrKU8iyJdnwnfgHI6k2hTlgqH4aSdjoTbg==", - "dev": true - } - } - }, "serialize-javascript": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz", @@ -28941,36 +15773,15 @@ "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", "dev": true }, - "set-value": { + "set-function-name": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz", - "integrity": "sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.1.tgz", + "integrity": "sha512-tMNCiqYVkXIZgc2Hnoy2IvC/f8ezc5koaRFkCjrpWzGpCd3qbZXPzVy9MAZzK1ch/X0jvSkojys3oqJN0qCmdA==", "dev": true, - "peer": true, "requires": { - "extend-shallow": "^2.0.1", - "is-extendable": "^0.1.1", - "is-plain-object": "^2.0.3", - "split-string": "^3.0.1" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", - "dev": true, - "peer": true, - "requires": { - "is-extendable": "^0.1.0" - } - }, - "is-extendable": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", - "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", - "dev": true, - "peer": true - } + "define-data-property": "^1.0.1", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.0" } }, "setprototypeof": { @@ -28979,26 +15790,11 @@ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", "dev": true }, - "shallow-clone": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", - "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", - "dev": true, - "peer": true, - "requires": { - "kind-of": "^6.0.2" - } - }, - "shallow-equals": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/shallow-equals/-/shallow-equals-1.0.0.tgz", - "integrity": "sha512-xd/FKcdmfmMbyYCca3QTVEJtqUOGuajNzvAX6nt8dXILwjAIEkfHc4hI8/JMGApAmb7VeULO0Q30NTxnbH/15g==", - "dev": true - }, "shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, "requires": { "shebang-regex": "^3.0.0" } @@ -29006,14 +15802,8 @@ "shebang-regex": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==" - }, - "shell-quote": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.7.3.tgz", - "integrity": "sha512-Vpfqwm4EnqGdlsBFNmHhxhElJYrdfcxPThu+ryKS5J8L/fhAwLazFZtq+S+TWZ9ANj2piSQLGj6NQg+lKPmxrw==", - "dev": true, - "peer": true + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true }, "shelljs": { "version": "0.8.5", @@ -29051,13 +15841,6 @@ "integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==", "dev": true }, - "sisteransi": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", - "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", - "dev": true, - "peer": true - }, "slash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", @@ -29075,255 +15858,6 @@ "is-fullwidth-code-point": "^3.0.0" } }, - "slide": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/slide/-/slide-1.1.6.tgz", - "integrity": "sha512-NwrtjCg+lZoqhFU8fOwl4ay2ei8PaqCBOUV3/ektPY9trO1yQ1oXEfmHAhKArUVUr/hOHvy5f6AdP17dCM0zMw==", - "dev": true - }, - "snapdragon": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz", - "integrity": "sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==", - "dev": true, - "peer": true, - "requires": { - "base": "^0.11.1", - "debug": "^2.2.0", - "define-property": "^0.2.5", - "extend-shallow": "^2.0.1", - "map-cache": "^0.2.2", - "source-map": "^0.5.6", - "source-map-resolve": "^0.5.0", - "use": "^3.1.0" - }, - "dependencies": { - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "peer": true, - "requires": { - "ms": "2.0.0" - } - }, - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA==", - "dev": true, - "peer": true, - "requires": { - "is-descriptor": "^0.1.0" - } - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", - "dev": true, - "peer": true, - "requires": { - "is-extendable": "^0.1.0" - } - }, - "is-accessor-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", - "integrity": "sha512-e1BM1qnDbMRG3ll2U9dSK0UMHuWOs3pY3AtcFsmvwPtKL3MML/Q86i+GilLfvqEs4GW+ExB91tQ3Ig9noDIZ+A==", - "dev": true, - "peer": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", - "dev": true, - "peer": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "is-data-descriptor": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", - "integrity": "sha512-+w9D5ulSoBNlmw9OHn3U2v51SyoCd0he+bB3xMl62oijhrspxowjU+AIcDY0N3iEJbUEkB15IlMASQsxYigvXg==", - "dev": true, - "peer": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", - "dev": true, - "peer": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "is-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", - "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", - "dev": true, - "peer": true, - "requires": { - "is-accessor-descriptor": "^0.1.6", - "is-data-descriptor": "^0.1.4", - "kind-of": "^5.0.0" - } - }, - "is-extendable": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", - "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", - "dev": true, - "peer": true - }, - "kind-of": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", - "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", - "dev": true, - "peer": true - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true, - "peer": true - }, - "source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", - "dev": true, - "peer": true - } - } - }, - "snapdragon-node": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/snapdragon-node/-/snapdragon-node-2.1.1.tgz", - "integrity": "sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==", - "dev": true, - "peer": true, - "requires": { - "define-property": "^1.0.0", - "isobject": "^3.0.0", - "snapdragon-util": "^3.0.1" - }, - "dependencies": { - "define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha512-cZTYKFWspt9jZsMscWo8sc/5lbPC9Q0N5nBLgb+Yd915iL3udB1uFgS3B8YCx66UVHq018DAVFoee7x+gxggeA==", - "dev": true, - "peer": true, - "requires": { - "is-descriptor": "^1.0.0" - } - } - } - }, - "snapdragon-util": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/snapdragon-util/-/snapdragon-util-3.0.1.tgz", - "integrity": "sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==", - "dev": true, - "peer": true, - "requires": { - "kind-of": "^3.2.0" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", - "dev": true, - "peer": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "socketcluster-client": { - "version": "14.3.2", - "resolved": "https://registry.npmjs.org/socketcluster-client/-/socketcluster-client-14.3.2.tgz", - "integrity": "sha512-xDtgW7Ss0ARlfhx53bJ5GY5THDdEOeJnT+/C9Rmrj/vnZr54xeiQfrCZJbcglwe732nK3V+uZq87IvrRl7Hn4g==", - "dev": true, - "requires": { - "buffer": "^5.2.1", - "clone": "2.1.1", - "component-emitter": "1.2.1", - "linked-list": "0.1.0", - "querystring": "0.2.0", - "sc-channel": "^1.2.0", - "sc-errors": "^2.0.1", - "sc-formatter": "^3.0.1", - "uuid": "3.2.1", - "ws": "^7.5.0" - }, - "dependencies": { - "buffer": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", - "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", - "dev": true, - "requires": { - "base64-js": "^1.3.1", - "ieee754": "^1.1.13" - } - }, - "uuid": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.2.1.tgz", - "integrity": "sha512-jZnMwlb9Iku/O3smGWvZhauCf6cvvpKi4BKRiliS3cxnI+Gz9j5MEpTz2UFuXiKPJocb7gnsLHwiS05ige5BEA==", - "dev": true - }, - "ws": { - "version": "7.5.8", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.8.tgz", - "integrity": "sha512-ri1Id1WinAX5Jqn9HejiGb8crfRio0Qgu8+MtL36rlTA6RLsMdWt1Az/19A2Qij6uSHUMphEFaTKa4WG+UNHNw==", - "dev": true, - "requires": {} - } - } - }, - "source-map": { - "version": "0.7.4", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", - "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", - "dev": true, - "peer": true - }, - "source-map-resolve": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.3.tgz", - "integrity": "sha512-Htz+RnsXWk5+P2slx5Jh3Q66vhQj1Cllm0zvnaY98+NFx+Dv2CF/f5O/t8x+KaNdrdIAsruNzoh/KpialbqAnw==", - "dev": true, - "peer": true, - "requires": { - "atob": "^2.1.2", - "decode-uri-component": "^0.2.0", - "resolve-url": "^0.2.1", - "source-map-url": "^0.4.0", - "urix": "^0.1.0" - } - }, "source-map-support": { "version": "0.5.21", "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", @@ -29344,36 +15878,12 @@ } } }, - "source-map-url": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.1.tgz", - "integrity": "sha512-cPiFOTLUKvJFIg4SKVScy4ilPPW6rFgMgfuZJPNoDuMs3nC1HbMUycBoJw77xFIp6z1UJQJOfx6C9GMH80DiTw==", - "dev": true, - "peer": true - }, - "split-string": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", - "integrity": "sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==", - "dev": true, - "peer": true, - "requires": { - "extend-shallow": "^3.0.0" - } - }, "split2": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/split2/-/split2-4.1.0.tgz", "integrity": "sha512-VBiJxFkxiXRlUIeyMQi8s4hgvKCSjtknJv/LVYbrgALPwf5zSKmEwV9Lst25AkvMDnvxODugjdl6KZgwKM1WYQ==", "dev": true }, - "sprintf-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", - "dev": true, - "peer": true - }, "sqlstring": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.1.tgz", @@ -29397,125 +15907,6 @@ "tweetnacl": "~0.14.0" } }, - "stackframe": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/stackframe/-/stackframe-1.3.4.tgz", - "integrity": "sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==", - "dev": true, - "peer": true - }, - "stacktrace-parser": { - "version": "0.1.10", - "resolved": "https://registry.npmjs.org/stacktrace-parser/-/stacktrace-parser-0.1.10.tgz", - "integrity": "sha512-KJP1OCML99+8fhOHxwwzyWrlUuVX5GQ0ZpJTd1DFXhdkrvg1szxfHhawXUZ3g9TkXORQd4/WG68jMlQZ2p8wlg==", - "dev": true, - "peer": true, - "requires": { - "type-fest": "^0.7.1" - }, - "dependencies": { - "type-fest": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.7.1.tgz", - "integrity": "sha512-Ne2YiiGN8bmrmJJEuTWTLJR32nh/JdL1+PSicowtNb0WFpn59GK8/lfD61bVtzguz7b3PBt74nxpv/Pw5po5Rg==", - "dev": true, - "peer": true - } - } - }, - "static-extend": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz", - "integrity": "sha512-72E9+uLc27Mt718pMHt9VMNiAL4LMsmDbBva8mxWUCkT07fSzEGMYUCk0XWY6lp0j6RBAG4cJ3mWuZv2OE3s0g==", - "dev": true, - "peer": true, - "requires": { - "define-property": "^0.2.5", - "object-copy": "^0.1.0" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA==", - "dev": true, - "peer": true, - "requires": { - "is-descriptor": "^0.1.0" - } - }, - "is-accessor-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", - "integrity": "sha512-e1BM1qnDbMRG3ll2U9dSK0UMHuWOs3pY3AtcFsmvwPtKL3MML/Q86i+GilLfvqEs4GW+ExB91tQ3Ig9noDIZ+A==", - "dev": true, - "peer": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", - "dev": true, - "peer": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "is-data-descriptor": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", - "integrity": "sha512-+w9D5ulSoBNlmw9OHn3U2v51SyoCd0he+bB3xMl62oijhrspxowjU+AIcDY0N3iEJbUEkB15IlMASQsxYigvXg==", - "dev": true, - "peer": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", - "dev": true, - "peer": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "is-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", - "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", - "dev": true, - "peer": true, - "requires": { - "is-accessor-descriptor": "^0.1.6", - "is-data-descriptor": "^0.1.4", - "kind-of": "^5.0.0" - } - }, - "kind-of": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", - "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", - "dev": true, - "peer": true - } - } - }, - "statuses": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", - "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", - "dev": true, - "peer": true - }, "stop-iteration-iterator": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.0.0.tgz", @@ -29581,55 +15972,36 @@ } }, "string.prototype.trim": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.7.tgz", - "integrity": "sha512-p6TmeT1T3411M8Cgg9wBTMRtY2q9+PNy9EV1i2lIXUN/btt763oIfxwN3RR8VU6wHX8j/1CFy0L+YuThm6bgOg==", + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.8.tgz", + "integrity": "sha512-lfjY4HcixfQXOfaqCvcBuOIapyaroTXhbkfJN3gcB1OtyupngWK4sEET9Knd0cXd28kTUqu/kHoV4HKSJdnjiQ==", "dev": true, "requires": { "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4" + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" } }, "string.prototype.trimend": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.6.tgz", - "integrity": "sha512-JySq+4mrPf9EsDBEDYMOb/lM7XQLulwg5R/m1r0PXEFqrV0qHvl58sdTilSXtKOflCsK2E8jxf+GKC0T07RWwQ==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.7.tgz", + "integrity": "sha512-Ni79DqeB72ZFq1uH/L6zJ+DKZTkOtPIHovb3YZHQViE+HDouuU4mBrLOLDn5Dde3RF8qw5qVETEjhu9locMLvA==", "dev": true, "requires": { "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4" + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" } }, "string.prototype.trimstart": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.6.tgz", - "integrity": "sha512-omqjMDaY92pbn5HOX7f9IccLA+U1tA9GvtU4JrodiXFfYB7jPzzHpRzpglLAjtUV6bB557zwClJezTqnAiYnQA==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.7.tgz", + "integrity": "sha512-NGhtDFu3jCEm7B4Fy0DpLewdJQOZcQ0rGbwQ/+stjnrp2i+rlKeCvos9hOIeCmqwratM47OBxY7uFZzjxHXmrg==", "dev": true, "requires": { "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4" - } - }, - "strip-ansi": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", - "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", - "dev": true, - "peer": true, - "requires": { - "ansi-regex": "^4.1.0" - }, - "dependencies": { - "ansi-regex": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz", - "integrity": "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==", - "dev": true, - "peer": true - } + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" } }, "strip-bom": { @@ -29638,13 +16010,6 @@ "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", "dev": true }, - "strip-eof": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", - "integrity": "sha512-7FCwGGmx8mD5xQd3RPUvnSpUXHM3BWuzjtpD4TXsfcZ9EL4azvVVUscFYwD9nx8Kh+uCBC00XBtAykoMHwTh8Q==", - "dev": true, - "peer": true - }, "strip-final-newline": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", @@ -29657,17 +16022,11 @@ "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", "dev": true }, - "sudo-prompt": { - "version": "9.2.1", - "resolved": "https://registry.npmjs.org/sudo-prompt/-/sudo-prompt-9.2.1.tgz", - "integrity": "sha512-Mu7R0g4ig9TUuGSxJavny5Rv0egCEtpZRNMrZaYS1vxkiIxGiGUwoezU3LazIQ+KE04hTrTfNPgxU5gzi7F5Pw==", - "dev": true, - "peer": true - }, "supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, "requires": { "has-flag": "^4.0.0" } @@ -29678,12 +16037,6 @@ "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", "dev": true }, - "symbol-observable": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.2.0.tgz", - "integrity": "sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==", - "dev": true - }, "tapable": { "version": "0.1.10", "resolved": "https://registry.npmjs.org/tapable/-/tapable-0.1.10.tgz", @@ -29711,26 +16064,6 @@ "tcomb": "^3.0.0" } }, - "temp": { - "version": "0.8.3", - "resolved": "https://registry.npmjs.org/temp/-/temp-0.8.3.tgz", - "integrity": "sha512-jtnWJs6B1cZlHs9wPG7BrowKxZw/rf6+UpGAkr8AaYmiTyTO7zQlLoST8zx/8TcUPnZmeBoB+H8ARuHZaSijVw==", - "dev": true, - "peer": true, - "requires": { - "os-tmpdir": "^1.0.0", - "rimraf": "~2.2.6" - }, - "dependencies": { - "rimraf": { - "version": "2.2.8", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.2.8.tgz", - "integrity": "sha512-R5KMKHnPAQaZMqLOsyuyUmcIjSeDm+73eoqQpaXA7AZ22BL+6C+1mcUscgOsNd8WVlJuvlgAPsegcx7pjlV0Dg==", - "dev": true, - "peer": true - } - } - }, "terser": { "version": "5.16.6", "resolved": "https://registry.npmjs.org/terser/-/terser-5.16.6.tgz", @@ -29791,13 +16124,6 @@ "integrity": "sha1-QlPinYywWPBIAmfFaYwOSSflS2o=", "dev": true }, - "throat": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/throat/-/throat-5.0.0.tgz", - "integrity": "sha512-fcwX4mndzpLQKBS1DVYhGAcYaYt7vsHNIvQV+WXMvnow5cgjPphq5CaayLaGsjRdSCKZFNGt7/GYAuXaNOiYCA==", - "dev": true, - "peer": true - }, "throttleit": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/throttleit/-/throttleit-1.0.0.tgz", @@ -29810,17 +16136,6 @@ "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", "dev": true }, - "through2": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", - "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", - "dev": true, - "peer": true, - "requires": { - "readable-stream": "~2.3.6", - "xtend": "~4.0.1" - } - }, "throwback": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/throwback/-/throwback-4.1.0.tgz", @@ -29834,9 +16149,9 @@ "dev": true }, "timezones.json": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/timezones.json/-/timezones.json-1.7.0.tgz", - "integrity": "sha512-qzreOP+2sNmx6S3Cys0pEyI6t4qX0VPb4oFYMePUrYvQeBszGazzNpvWuAAGENGmpw7GoAND255NTkEQeXryvg==", + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/timezones.json/-/timezones.json-1.7.1.tgz", + "integrity": "sha512-4dB58ulcrRWfiGufzlofLG45RIoalCTZiFUc7tnj0g8za0CpNTyIOVlspg1JD7OFyDeW5up3ntlkukizwB0IJA==", "dev": true }, "tmp": { @@ -29859,13 +16174,6 @@ } } }, - "tmpl": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", - "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", - "dev": true, - "peer": true - }, "to-fast-properties": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", @@ -29873,45 +16181,11 @@ "dev": true, "peer": true }, - "to-object-path": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz", - "integrity": "sha512-9mWHdnGRuh3onocaHzukyvCZhzvr6tiflAy/JRFXcJX0TjgfWA9pk9t8CMbzmBE4Jfw58pXbkngtBtqYxzNEyg==", - "dev": true, - "peer": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", - "dev": true, - "peer": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "to-regex": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/to-regex/-/to-regex-3.0.2.tgz", - "integrity": "sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==", - "dev": true, - "peer": true, - "requires": { - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "regex-not": "^1.0.2", - "safe-regex": "^1.1.0" - } - }, "to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, "requires": { "is-number": "^7.0.0" } @@ -29923,21 +16197,31 @@ "dev": true }, "tough-cookie": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", - "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.3.tgz", + "integrity": "sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw==", "dev": true, "requires": { - "psl": "^1.1.28", - "punycode": "^2.1.1" + "psl": "^1.1.33", + "punycode": "^2.1.1", + "universalify": "^0.2.0", + "url-parse": "^1.5.3" + }, + "dependencies": { + "universalify": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", + "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", + "dev": true + } } }, - "tr46": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "ts-api-utils": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.0.3.tgz", + "integrity": "sha512-wNMeqtMz5NtwpT/UZGY5alT+VoKdSsOOP/kqHFcUW1P/VRhH2wJ48+DN2WwUliNbQ976ETwDL0Ifd2VVvgonvg==", "dev": true, - "peer": true + "requires": {} }, "ts-expect": { "version": "1.3.0", @@ -29968,30 +16252,6 @@ } } }, - "tslib": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", - "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==", - "dev": true, - "peer": true - }, - "tsutils": { - "version": "3.21.0", - "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", - "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", - "dev": true, - "requires": { - "tslib": "^1.8.1" - }, - "dependencies": { - "tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", - "dev": true - } - } - }, "tunnel-agent": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", @@ -30038,6 +16298,42 @@ "mime-types": "~2.1.24" } }, + "typed-array-buffer": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.0.tgz", + "integrity": "sha512-Y8KTSIglk9OZEr8zywiIHG/kmQ7KWyjseXs1CbSo8vC42w7hg2HgYTxSWwP0+is7bWDc1H+Fo026CpHFwm8tkw==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.2.1", + "is-typed-array": "^1.1.10" + } + }, + "typed-array-byte-length": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.0.tgz", + "integrity": "sha512-Or/+kvLxNpeQ9DtSydonMxCx+9ZXOswtwJn17SNLvhptaXYDJvkFFP5zbfU/uLmvnBJlI4yrnXRxpdWH/M5tNA==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "for-each": "^0.3.3", + "has-proto": "^1.0.1", + "is-typed-array": "^1.1.10" + } + }, + "typed-array-byte-offset": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.0.tgz", + "integrity": "sha512-RD97prjEt9EL8YgAgpOkf3O4IF9lhJFr9g0htQkm0rchFp/Vx7LW5Q8fSXXub7BXAODyUQohRMyOc3faCPd0hg==", + "dev": true, + "requires": { + "available-typed-arrays": "^1.0.5", + "call-bind": "^1.0.2", + "for-each": "^0.3.3", + "has-proto": "^1.0.1", + "is-typed-array": "^1.1.10" + } + }, "typed-array-length": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.4.tgz", @@ -30050,38 +16346,11 @@ } }, "typescript": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.0.4.tgz", - "integrity": "sha512-cW9T5W9xY37cc+jfEnaUvX91foxtHkza3Nw3wkoF4sSlKn0MONdkdEndig/qPBWXNkmplh3NzayQzCiHM4/hqw==", + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz", + "integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==", "dev": true }, - "uglify-es": { - "version": "3.3.9", - "resolved": "https://registry.npmjs.org/uglify-es/-/uglify-es-3.3.9.tgz", - "integrity": "sha512-r+MU0rfv4L/0eeW3xZrd16t4NZfK8Ld4SWVglYBb7ez5uXFWHuVRs6xCTrf1yirs9a4j4Y27nn7SRfO6v67XsQ==", - "dev": true, - "peer": true, - "requires": { - "commander": "~2.13.0", - "source-map": "~0.6.1" - }, - "dependencies": { - "commander": { - "version": "2.13.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.13.0.tgz", - "integrity": "sha512-MVuS359B+YzaWqjCL/c+22gfryv+mCBPHAv3zyVI2GN8EY6IRP8VwtasXn8jyyhvvq84R4ImN1OKRtcbIasjYA==", - "dev": true, - "peer": true - }, - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "peer": true - } - } - }, "unbox-primitive": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", @@ -30094,64 +16363,11 @@ "which-boxed-primitive": "^1.0.2" } }, - "unicode-canonical-property-names-ecmascript": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz", - "integrity": "sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ==", - "dev": true, - "peer": true - }, - "unicode-match-property-ecmascript": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz", - "integrity": "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==", - "dev": true, - "peer": true, - "requires": { - "unicode-canonical-property-names-ecmascript": "^2.0.0", - "unicode-property-aliases-ecmascript": "^2.0.0" - } - }, - "unicode-match-property-value-ecmascript": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.1.0.tgz", - "integrity": "sha512-qxkjQt6qjg/mYscYMC0XKRn3Rh0wFPlfxB0xkt9CfyTvpX1Ra0+rAmdX2QyAobptSEvuy4RtpPRui6XkV+8wjA==", - "dev": true, - "peer": true - }, - "unicode-property-aliases-ecmascript": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz", - "integrity": "sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==", - "dev": true, - "peer": true - }, - "union-value": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz", - "integrity": "sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg==", - "dev": true, - "peer": true, - "requires": { - "arr-union": "^3.1.0", - "get-value": "^2.0.6", - "is-extendable": "^0.1.1", - "set-value": "^2.0.1" - }, - "dependencies": { - "is-extendable": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", - "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", - "dev": true, - "peer": true - } - } - }, "universalify": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", - "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==" + "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", + "dev": true }, "unpipe": { "version": "1.0.0", @@ -30159,50 +16375,6 @@ "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=", "dev": true }, - "unset-value": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz", - "integrity": "sha512-PcA2tsuGSF9cnySLHTLSh2qrQiJ70mn+r+Glzxv2TWZblxsxCC52BDlZoPCsz7STd9pN7EZetkWZBAvk4cgZdQ==", - "dev": true, - "peer": true, - "requires": { - "has-value": "^0.3.1", - "isobject": "^3.0.0" - }, - "dependencies": { - "has-value": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/has-value/-/has-value-0.3.1.tgz", - "integrity": "sha512-gpG936j8/MzaeID5Yif+577c17TxaDmhuyVgSwtnL/q8UUTySg8Mecb+8Cf1otgLoD7DDH75axp86ER7LFsf3Q==", - "dev": true, - "peer": true, - "requires": { - "get-value": "^2.0.3", - "has-values": "^0.1.4", - "isobject": "^2.0.0" - }, - "dependencies": { - "isobject": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", - "integrity": "sha512-+OUdGJlgjOBZDfxnDjYYG6zp487z0JGNQq3cYQYg5f5hKR+syHMsaztzGeml/4kGG55CSpKSpWTY+jYGgsHLgA==", - "dev": true, - "peer": true, - "requires": { - "isarray": "1.0.0" - } - } - } - }, - "has-values": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/has-values/-/has-values-0.1.4.tgz", - "integrity": "sha512-J8S0cEdWuQbqD9//tlZxiMuMNmxB8PlEwvYwuxsTmR1G5RXUePEX/SJn7aD0GMLieuZYSwNH0cQuJGwnYunXRQ==", - "dev": true, - "peer": true - } - } - }, "untildify": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/untildify/-/untildify-4.0.0.tgz", @@ -30229,13 +16401,6 @@ "punycode": "^2.1.0" } }, - "urix": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz", - "integrity": "sha512-Am1ousAhSLBeB9cG/7k7r2R0zj50uDRlZHPGbazid5s9rlF1F/QKYObEKSIunSjIOkJZqwRRLpvewjEkM7pSqg==", - "dev": true, - "peer": true - }, "url": { "version": "0.10.3", "resolved": "https://registry.npmjs.org/url/-/url-0.10.3.tgz", @@ -30254,20 +16419,15 @@ } } }, - "use": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz", - "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==", + "url-parse": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", "dev": true, - "peer": true - }, - "use-sync-external-store": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz", - "integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==", - "dev": true, - "peer": true, - "requires": {} + "requires": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } }, "util": { "version": "0.10.4", @@ -30299,9 +16459,9 @@ "dev": true }, "uuid": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz", - "integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==", + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", "dev": true }, "validator": { @@ -30327,23 +16487,6 @@ "extsprintf": "^1.2.0" } }, - "vlq": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/vlq/-/vlq-1.0.1.tgz", - "integrity": "sha512-gQpnTgkubC6hQgdIcRdYGDSDc+SaujOdyesZQMv6JlfQee/9Mp0Qhnys6WxDWvQnL5WZdT7o2Ul187aSt0Rq+w==", - "dev": true, - "peer": true - }, - "walker": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", - "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", - "dev": true, - "peer": true, - "requires": { - "makeerror": "1.0.12" - } - }, "watchpack": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz", @@ -30355,23 +16498,6 @@ "graceful-fs": "^4.1.2" } }, - "wcwidth": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", - "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==", - "dev": true, - "peer": true, - "requires": { - "defaults": "^1.0.3" - } - }, - "webidl-conversions": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", - "dev": true, - "peer": true - }, "webpack": { "version": "5.76.1", "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.76.1.tgz", @@ -30454,28 +16580,11 @@ "dev": true, "peer": true }, - "whatwg-fetch": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.2.tgz", - "integrity": "sha512-bJlen0FcuU/0EMLrdbJ7zOnW6ITZLrZMIarMUVmdKtsGvZna8vxKYaexICWPfZ8qwf9fzNq+UEIZrnSaApt6RA==", - "dev": true, - "peer": true - }, - "whatwg-url": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", - "dev": true, - "peer": true, - "requires": { - "tr46": "~0.0.3", - "webidl-conversions": "^3.0.0" - } - }, "which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, "requires": { "isexe": "^2.0.0" } @@ -30493,6 +16602,34 @@ "is-symbol": "^1.0.3" } }, + "which-builtin-type": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.1.3.tgz", + "integrity": "sha512-YmjsSMDBYsM1CaFiayOVT06+KJeXf0o5M/CAd4o1lTadFAtacTUM49zoYxr/oroopFDfhvN6iEcBxUyc3gvKmw==", + "dev": true, + "requires": { + "function.prototype.name": "^1.1.5", + "has-tostringtag": "^1.0.0", + "is-async-function": "^2.0.0", + "is-date-object": "^1.0.5", + "is-finalizationregistry": "^1.0.2", + "is-generator-function": "^1.0.10", + "is-regex": "^1.1.4", + "is-weakref": "^1.0.2", + "isarray": "^2.0.5", + "which-boxed-primitive": "^1.0.2", + "which-collection": "^1.0.1", + "which-typed-array": "^1.1.9" + }, + "dependencies": { + "isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true + } + } + }, "which-collection": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.1.tgz", @@ -30512,25 +16649,18 @@ "dev": true }, "which-typed-array": { - "version": "1.1.9", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.9.tgz", - "integrity": "sha512-w9c4xkx6mPidwp7180ckYWfMmvxpjlZuIudNtDf4N/tTAUB8VJbX25qZoAsrtGuYNnGw3pa0AXgbGKRB8/EceA==", + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.11.tgz", + "integrity": "sha512-qe9UWWpkeG5yzZ0tNYxDmd7vo58HDBc39mZ0xWWpolAGADdFOzkfamWLDxkOWcvHQKVmdTyQdLD4NOfjLWTKew==", "dev": true, "requires": { "available-typed-arrays": "^1.0.5", "call-bind": "^1.0.2", "for-each": "^0.3.3", "gopd": "^1.0.1", - "has-tostringtag": "^1.0.0", - "is-typed-array": "^1.1.10" + "has-tostringtag": "^1.0.0" } }, - "word-wrap": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", - "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", - "dev": true - }, "workerpool": { "version": "6.2.1", "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.2.1.tgz", @@ -30562,29 +16692,8 @@ "wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" - }, - "write-file-atomic": { - "version": "2.4.3", - "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-2.4.3.tgz", - "integrity": "sha512-GaETH5wwsX+GcnzhPgKcKjJ6M2Cq3/iZp1WyY/X1CSqrW+jVNM9Y7D8EC2sM4ZG/V8wZlSniJnCKWPmBYAucRQ==", - "dev": true, - "peer": true, - "requires": { - "graceful-fs": "^4.1.11", - "imurmurhash": "^0.1.4", - "signal-exit": "^3.0.2" - } - }, - "ws": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ws/-/ws-6.2.2.tgz", - "integrity": "sha512-zmhltoSR8u1cnDsD43TX59mzoMZsLKqUweyYBAIvTngR3shc0W6aOZylZmq/7hqyVxPdi+5Ud2QInblgyE72fw==", - "dev": true, - "peer": true, - "requires": { - "async-limiter": "~1.0.0" - } + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true }, "xml": { "version": "1.0.1", @@ -30626,11 +16735,6 @@ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", "dev": true }, - "yaml": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.2.2.tgz", - "integrity": "sha512-CBKFWExMn46Foo4cldiChEzn7S7SRV+wqiluAb6xmueD/fGyRHIhX8m14vVGgeFWjN540nKCNVj6P21eQjgTuA==" - }, "yargs": { "version": "17.7.2", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", diff --git a/e2e-tests/cypress/package.json b/e2e-tests/cypress/package.json index 70d6741c0b6..bc832c21ae4 100644 --- a/e2e-tests/cypress/package.json +++ b/e2e-tests/cypress/package.json @@ -1,64 +1,64 @@ { "devDependencies": { - "@babel/eslint-parser": "7.21.8", - "@babel/eslint-plugin": "7.19.1", - "@cypress/request": "2.88.11", - "@mattermost/types": "7.10.0", - "@testing-library/cypress": "9.0.0", - "@types/async": "3.2.20", - "@types/authenticator": "1.1.1", - "@types/express": "4.17.17", - "@types/fs-extra": "11.0.1", - "@types/lodash": "4.14.194", + "@babel/eslint-parser": "7.22.15", + "@babel/eslint-plugin": "7.22.10", + "@cypress/request": "3.0.1", + "@mattermost/client": "9.0.0", + "@mattermost/types": "9.0.0", + "@testing-library/cypress": "10.0.1", + "@types/async": "3.2.21", + "@types/authenticator": "1.1.2", + "@types/express": "4.17.18", + "@types/fs-extra": "11.0.2", + "@types/lodash": "4.14.199", "@types/lodash.intersection": "4.4.7", "@types/lodash.mapkeys": "4.6.7", "@types/lodash.without": "4.4.7", - "@types/mime-types": "2.1.1", - "@types/mochawesome": "6.2.1", - "@types/pdf-parse": "1.1.1", - "@types/recursive-readdir": "2.2.1", - "@types/shelljs": "0.8.12", - "@types/uuid": "9.0.1", - "@typescript-eslint/eslint-plugin": "5.59.2", - "@typescript-eslint/parser": "5.59.2", + "@types/mime-types": "2.1.2", + "@types/mochawesome": "6.2.2", + "@types/pdf-parse": "1.1.2", + "@types/recursive-readdir": "2.2.2", + "@types/shelljs": "0.8.13", + "@types/uuid": "9.0.4", + "@typescript-eslint/eslint-plugin": "6.7.4", + "@typescript-eslint/parser": "6.7.4", "async": "3.2.4", "authenticator": "1.1.5", - "aws-sdk": "2.1371.0", - "axios": "1.4.0", - "axios-retry": "3.4.0", - "chai": "4.3.7", + "aws-sdk": "2.1468.0", + "axios": "1.5.1", + "axios-retry": "3.8.0", + "chai": "4.3.10", "chalk": "4.1.2", "client-oauth2": "github:larkox/js-client-oauth2#e24e2eb5dfcbbbb3a59d095e831dbe0012b0ac49", "cross-env": "7.0.3", - "cypress": "12.11.0", + "cypress": "13.3.0", "cypress-file-upload": "5.0.8", "cypress-multi-reporters": "1.6.3", "cypress-plugin-tab": "1.0.5", - "cypress-wait-until": "1.7.2", - "dayjs": "1.11.7", + "cypress-wait-until": "2.0.1", + "dayjs": "1.11.10", "deepmerge": "4.3.1", - "dotenv": "16.0.3", - "eslint": "8.39.0", - "eslint-import-resolver-webpack": "0.13.2", - "eslint-plugin-cypress": "2.13.3", + "dotenv": "16.3.1", + "eslint": "8.50.0", + "eslint-import-resolver-webpack": "0.13.7", + "eslint-plugin-cypress": "2.15.1", "eslint-plugin-header": "3.1.1", - "eslint-plugin-import": "2.27.5", + "eslint-plugin-import": "2.28.1", "eslint-plugin-mattermost": "github:mattermost/eslint-plugin-mattermost#5b0c972eacf19286e4c66221b39113bf8728a99e", "eslint-plugin-no-only-tests": "3.1.0", - "eslint-plugin-react": "7.32.2", + "eslint-plugin-react": "7.33.2", "express": "4.18.2", "extract-zip": "2.0.1", - "knex": "2.4.2", + "knex": "2.5.1", "localforage": "1.10.0", "lodash.intersection": "4.4.0", "lodash.mapkeys": "4.6.0", "lodash.without": "4.4.0", "lodash.xor": "4.5.0", - "mattermost-redux": "5.33.1", "mime": "3.0.0", "mime-types": "2.1.35", "mocha": "10.2.0", - "mocha-junit-reporter": "2.2.0", + "mocha-junit-reporter": "2.2.1", "mocha-multi-reporters": "1.5.1", "mochawesome": "7.1.3", "mochawesome-merge": "4.3.0", @@ -67,16 +67,23 @@ "mysql": "2.18.1", "path": "0.12.7", "pdf-parse": "1.1.1", - "pg": "8.10.0", + "pg": "8.11.3", "recursive-readdir": "2.2.3", "shelljs": "0.8.5", - "timezones.json": "1.7.0", - "typescript": "5.0.4", - "uuid": "9.0.0", + "timezones.json": "1.7.1", + "typescript": "5.2.2", + "uuid": "9.0.1", "yargs": "17.7.2" }, + "overrides": { + "@mattermost/client": { + "typescript": "^5.0.4" + }, + "@mattermost/types": { + "typescript": "^5.0.4" + } + }, "scripts": { - "postinstall": "patch-package", "check-types": "tsc -b", "cypress:open": "cross-env TZ=Etc/UTC cypress open", "cypress:run": "cross-env TZ=Etc/UTC cypress run", @@ -92,8 +99,5 @@ "uniq-meta": "grep -r \"^// $META:\" cypress | grep -ow '@\\w*' | sort | uniq", "check": "eslint --ext .js,.ts . --quiet --cache", "fix": "eslint --ext .js,.ts . --quiet --fix --cache" - }, - "dependencies": { - "patch-package": "7.0.0" } } diff --git a/e2e-tests/cypress/patches/@testing-library+cypress+9.0.0.patch b/e2e-tests/cypress/patches/@testing-library+cypress+9.0.0.patch deleted file mode 100644 index 579e28d8660..00000000000 --- a/e2e-tests/cypress/patches/@testing-library+cypress+9.0.0.patch +++ /dev/null @@ -1,13 +0,0 @@ -diff --git a/node_modules/@testing-library/cypress/dist/index.js b/node_modules/@testing-library/cypress/dist/index.js -index 9a03c94..b2d3aac 100644 ---- a/node_modules/@testing-library/cypress/dist/index.js -+++ b/node_modules/@testing-library/cypress/dist/index.js -@@ -38,7 +38,7 @@ function createQuery(queryName, implementationName) { - }; - const log = options.log !== false && Cypress.log({ - name: queryName, -- type: this.get('prev').get('chainerId') === this.get('chainerId') ? 'child' : 'parent', -+ type: this.get('prev') && this.get('prev').get('chainerId') === this.get('chainerId') ? 'child' : 'parent', - message: inputArr, - timeout: options.timeout, - consoleProps: () => consoleProps diff --git a/e2e-tests/cypress/tests/integration/boards/card_badges_spec.ts b/e2e-tests/cypress/tests/integration/boards/card_badges_spec.ts deleted file mode 100644 index 4f2a2c13545..00000000000 --- a/e2e-tests/cypress/tests/integration/boards/card_badges_spec.ts +++ /dev/null @@ -1,76 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -// *************************************************************** -// - [#] indicates a test step (e.g. # Go to a page) -// - [*] indicates an assertion (e.g. * Check the title) -// - Use element ID when selecting an element. Create one if none. -// *************************************************************** - -// Stage: @prod -// Group: @boards - -describe('Card badges', () => { - before(() => { - // # Login as new user - cy.apiInitSetup({loginAfter: true}); - cy.clearLocalStorage(); - }); - - it('MM-T5395 Shows and hides card badges', () => { - cy.visit('/boards'); - - // Create new board - cy.uiCreateNewBoard('Testing'); - - // Add a new card - cy.uiAddNewCard('Card'); - - // Add some comments - cy.log('**Add some comments**'); - addComment('Some comment'); - addComment('Another comment'); - addComment('Additional comment'); - - // Add card description - cy.log('**Add card description**'); - cy.findByText('Add a description...').click(); - cy.findByRole('combobox').type('## Header\n- [ ] one\n- [x] two{esc}'); - - // Add checkboxes - cy.log('**Add checkboxes**'); - cy.findByRole('button', {name: 'Add content'}).click(); - cy.findByRole('button', {name: 'checkbox'}).click(); - cy.focused().type('three{enter}'); - cy.focused().type('four{enter}'); - cy.focused().type('{esc}'); - cy.findByDisplayValue('three').prev().click(); - - // Close card dialog - cy.log('**Close card dialog**'); - cy.findByRole('button', {name: 'Close dialog'}).click(); - cy.findByRole('dialog').should('not.exist'); - - // Show card badges - cy.log('**Show card badges**'); - cy.findByRole('button', {name: 'Properties menu'}).click(); - cy.findByRole('button', {name: 'Comments and description'}).click(); - cy.findByTitle('This card has a description').should('exist'); - cy.findByTitle('Comments').contains('3').should('exist'); - cy.findByTitle('Checkboxes').contains('2/4').should('exist'); - - // Hide card badges - cy.log('**Hide card badges**'); - cy.findByRole('button', {name: 'Comments and description'}).click(); - cy.findByRole('button', {name: 'Properties menu'}).click(); - cy.findByTitle('This card has a description').should('not.exist'); - cy.findByTitle('Comments').should('not.exist'); - cy.findByTitle('Checkboxes').should('not.exist'); - }); - - const addComment = (text: string) => { - cy.findByText('Add a comment...').click(); - cy.findByRole('combobox').type(text).blur(); - cy.findByRole('button', {name: 'Send'}).click(); - }; -}); diff --git a/e2e-tests/cypress/tests/integration/boards/card_urlproperty_spec.ts b/e2e-tests/cypress/tests/integration/boards/card_urlproperty_spec.ts deleted file mode 100644 index 2c549578485..00000000000 --- a/e2e-tests/cypress/tests/integration/boards/card_urlproperty_spec.ts +++ /dev/null @@ -1,118 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -// *************************************************************** -// - [#] indicates a test step (e.g. # Go to a page) -// - [*] indicates an assertion (e.g. * Check the title) -// - Use element ID when selecting an element. Create one if none. -// *************************************************************** - -// Stage: @prod -// Group: @boards - -describe('Card URL Property', () => { - before(() => { - // # Login as new user - cy.apiInitSetup({loginAfter: true}); - cy.clearLocalStorage(); - }); - - const url = 'https://mattermost.com'; - const changedURL = 'https://mattermost.com/blog'; - - it('MM-T5396 Allows to create and edit URL property', () => { - cy.visit('/boards'); - - // Create new board - cy.uiCreateNewBoard('Testing'); - - // Add a new card - cy.uiAddNewCard('Card'); - - // Add URL property - cy.log('**Add URL property**'); - cy.findByRole('button', {name: '+ Add a property'}).click(); - cy.findByRole('button', {name: 'URL'}).click(); - cy.findByRole('textbox', {name: 'URL'}).type('{enter}'); - - // Enter URL - cy.log('**Enter URL**'); - cy.findByPlaceholderText('Empty').type(`${url}{enter}`); - - // Check buttons - cy.log('**Check link**'); - cy.get('.URLProperty').trigger('mouseover'); - - cy.log('**Check buttons**'); - - // Change URL - cy.log('**Change URL**'); - - cy.get('.URLProperty Button[title=\'Edit\']').click({force: true}); - cy.findByRole('textbox', {name: url}).clear().type(`${changedURL}{enter}`); - cy.findByRole('link', {name: changedURL}).should('exist'); - - // Close card dialog - cy.log('**Close card dialog**'); - cy.findByRole('button', {name: 'Close dialog'}).click(); - cy.findByRole('dialog').should('not.exist'); - - // Show URL property - showURLProperty(); - - // Copy URL to clipboard - cy.log('**Copy URL to clipboard**'); - cy.document().then((doc) => cy.spy(doc, 'execCommand')).as('exec'); - cy.get('.URLProperty Button[title=\'Edit\']').should('not.exist'); - cy.get('.URLProperty Button[title=\'Copy\']').click({force: true}); - cy.findByText('Copied!').should('exist'); - cy.findByText('Copied!').should('not.exist'); - cy.get('@exec').should('have.been.calledOnceWith', 'copy'); - - // Add table view - addView('Table'); - - // Check buttons - cy.log('**Check buttons**'); - cy.get('.URLProperty Button[title=\'Edit\']').should('exist'); - cy.get('.URLProperty Button[title=\'Copy\']').should('exist'); - cy.findByRole('button', {name: 'Copy'}).should('not.exist'); - - // Add gallery view - addView('Gallery'); - showURLProperty(); - - // Check buttons - cy.log('**Check buttons**'); - cy.get('.URLProperty Button[title=\'Edit\']').should('not.exist'); - cy.get('.URLProperty Button[title=\'Copy\']').should('exist'); - - // Add calendar view - addView('Calendar'); - showURLProperty(); - - // Check buttons - cy.log('**Check buttons**'); - cy.get('.URLProperty Button[title=\'Edit\']').should('not.exist'); - cy.get('.URLProperty Button[title=\'Copy\']').should('exist'); - }); - - type ViewType = 'Board' | 'Table' | 'Gallery' | 'Calendar' - - const addView = (type: ViewType) => { - cy.log(`**Add ${type} view**`); - - cy.findByRole('button', {name: 'View menu'}).click(); - cy.findByText('Add view').trigger('mouseover'); - cy.findByRole('button', {name: type}).click(); - cy.findByRole('textbox', {name: `${type} view`}).should('exist'); - }; - - const showURLProperty = () => { - cy.log('**Show URL property**'); - cy.findByRole('button', {name: 'Properties'}).click(); - cy.findByRole('button', {name: 'URL'}).click(); - cy.findByRole('button', {name: 'Properties'}).click(); - cy.findByRole('link', {name: changedURL}).should('exist'); - }; -}); diff --git a/e2e-tests/cypress/tests/integration/boards/channel_header_spec.ts b/e2e-tests/cypress/tests/integration/boards/channel_header_spec.ts deleted file mode 100644 index 68d71d55298..00000000000 --- a/e2e-tests/cypress/tests/integration/boards/channel_header_spec.ts +++ /dev/null @@ -1,90 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -// *************************************************************** -// - [#] indicates a test step (e.g. # Go to a page) -// - [*] indicates an assertion (e.g. * Check the title) -// *************************************************************** - -// Stage: @prod -// Group: @boards - -describe('channels > channel header', {testIsolation: true}, () => { - let testTeam: {name: string}; - let testUser: {username: string}; - - before(() => { - cy.apiInitSetup().then(({team, user}) => { - testTeam = team; - testUser = user; - - // # Login as testUser - cy.apiLogin(testUser); - }); - }); - - describe('App Bar enabled', () => { - it('webapp should hide the Boards channel header button', () => { - cy.apiAdminLogin(); - cy.apiUpdateConfig({ExperimentalSettings: {EnableAppBar: true}}); - - // # Login as testUser - cy.apiLogin(testUser); - - // # Navigate directly to a channel - cy.visit(`/${testTeam.name}/channels/town-square`); - - // * Verify channel header button is not showing - cy.get('#channel-header').within(() => { - cy.get('[data-testid="boardsIcon"]').should('not.exist'); - }); - }); - }); - - describe('App Bar disabled', () => { - beforeEach(() => { - cy.apiAdminLogin(); - cy.apiUpdateConfig({ExperimentalSettings: {EnableAppBar: false}}); - - // # Login as testUser - cy.apiLogin(testUser); - }); - - it('webapp should show the Boards channel header button', () => { - // # Navigate directly to a channel - cy.visit(`/${testTeam.name}/channels/town-square`); - - // * Verify channel header button is showing - cy.get('#channel-header').within(() => { - cy.get('#incidentIcon').should('exist'); - }); - }); - - it('tooltip text should show "Boards" for Boards channel header button', () => { - // # Navigate directly to a channel - cy.visit(`/${testTeam.name}/channels/town-square`); - - // # Hover over the channel header icon - cy.get('#channel-header').within(() => { - cy.get('[data-testid="boardsIcon"]').trigger('mouseover'); - }); - - // * Verify tooltip text - cy.get('#pluginTooltip').contains('Boards'); - }); - - it('webapp should make the Boards channel header button active when opened', () => { - // # Navigate directly to a channel - cy.visit(`/${testTeam.name}/channels/town-square`); - - cy.get('#channel-header').within(() => { - // # Click the channel header button - cy.get('[data-testid="boardsIcon"]').as('icon').click(); - - // * Verify channel header button is showing active className - cy.get('@icon').parent(). - should('have.class', 'channel-header__icon--active-inverted'); - }); - }); - }); -}); diff --git a/e2e-tests/cypress/tests/integration/boards/create_board_spec.ts b/e2e-tests/cypress/tests/integration/boards/create_board_spec.ts deleted file mode 100644 index 0d0e0b2ad8e..00000000000 --- a/e2e-tests/cypress/tests/integration/boards/create_board_spec.ts +++ /dev/null @@ -1,339 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -// *************************************************************** -// - [#] indicates a test step (e.g. # Go to a page) -// - [*] indicates an assertion (e.g. * Check the title) -// - Use element ID when selecting an element. Create one if none. -// *************************************************************** - -// Stage: @prod -// Group: @boards - -import timeouts from '../../fixtures/timeouts'; - -describe('Create and delete board / card', () => { - const timestamp = new Date().toLocaleString(); - const boardTitle = `Test Board (${timestamp})`; - const cardTitle = `Test Card (${timestamp})`; - - beforeEach(() => { - // # Login as new user - cy.apiAdminLogin().apiInitSetup({loginAfter: true}); - cy.clearLocalStorage(); - }); - - it('MM-T4274 Create an Empty Board', () => { - cy.visit('/boards'); - - // Tests for template selector - cy.findByText('Use this template').should('exist').click(); - - // Some options are present - cy.contains('Meeting Agenda').should('exist'); - cy.contains('Personal Goals').should('exist'); - cy.contains('Project Tasks').should('exist'); - - // Create empty board - cy.findByText('Create an empty board').should('exist').click({force: true}); - cy.get('.BoardComponent').should('exist'); - - // Change Title - cy.findByPlaceholderText('Untitled board').should('be.visible').wait(timeouts.HALF_SEC).as('editableTitle'); - cy.get('@editableTitle').should('be.visible'). - clear(). - type('Testing'). - type('{enter}'). - should('have.value', 'Testing'); - }); - - it('MM-T4275 Set up Board description', () => { - cy.visit('/boards'); - - // # Create an empty board and change tile to Testing - cy.findByText('Create an empty board').should('exist').click({force: true}); - cy.get('.BoardComponent').should('exist'); - - // # Change Title - cy.findByPlaceholderText('Untitled board').should('be.visible').wait(timeouts.HALF_SEC); - - // * Assert that the title is changed to "testing" - cy.findByPlaceholderText('Untitled board'). - clear(). - type('Testing'). - type('{enter}'). - should('have.value', 'Testing'); - - // # "Add icon" and "Show description" options appear - cy.findByText('Add icon').should('exist').click({force: true}); - cy.findByText('show description').should('exist').click({force: true}); - - // # Click on "Add a description" below the board title and type "for testing purposes only" - cy.findByText('Add a description...').should('be.visible').wait(timeouts.HALF_SEC); - - // * Assert that the editable description should be visible - cy.findByText('Add a description...').should('be.visible'); - cy.findByText('Add a description...').click({force: true}); - cy.get('.description'). - click(). - get('.description .MarkdownEditorInput'). - type('for testing purposes only'); - - // # Click to other element to give some time for the description to be saved. - cy.findByPlaceholderText('Untitled board').click(); - - // * Assert that the description is changed to "for testing purposes only" - cy.findByText('for testing purposes only').should('be.visible'); - - // # Hide Description options should appear and click on it to hide description - cy.findByText('hide description').should('exist').click({force: true}); - - // * Assert that description should not appear" - cy.get('.description').should('not.exist'); - - // # Show Description options should appear and click on it to show description - cy.findByText('show description').should('exist').click({force: true}); - - // * Assert that the description "for testing purposes should be visible" - cy.findByText('for testing purposes only').should('be.visible'); - }); - - it('MM-T4276 Set up Board emoji', () => { - cy.visit('/boards'); - - // # Create an empty board and change tile to Testing - cy.findByText('Create an empty board').should('exist').click({force: true}); - cy.get('.BoardComponent').should('exist'); - - // # Change Title - cy.findByPlaceholderText('Untitled board').should('be.visible').wait(timeouts.HALF_SEC); - - // * Assert that the title is changed to "testing" - cy.findByPlaceholderText('Untitled board'). - clear(). - type('Testing'). - type('{enter}'). - should('have.value', 'Testing'); - - // # "Add icon" and "Show description" options appear - cy.findByText('Add icon').should('exist'); - cy.findByText('show description').should('exist'); - - // # Click on "Add icon" - cy.findByText('Add icon').should('exist').click({force: true}); - - // * Assert that a random emoji is selected and added at the beginning of the board title - cy.get('.IconSelector').should('exist'); - - // # Click on the emoji next to the board title - cy.get('.IconSelector .MenuWrapper').should('exist').click({force: true}); - - // * Assert that Dropdown menu with 3 options appears - cy.findByText('Random').should('exist'); - cy.findByText('Pick icon').should('exist'); - cy.findByText('Remove icon').should('exist'); - - // # Hover your mouse over the "Pick Icon" option - cy.findByText('Pick icon').trigger('mouseover'); - - // * Assert that emoji picker menu appears - cy.get('.IconSelector .menu-contents').should('exist'); - - // # Click on the emoji from the picker - cy.get('.EmojiPicker').should('exist').and('be.visible').within(() => { - // # Click on the emoji - cy.get("[aria-label='😀, grinning']").should('exist'); - cy.get("[aria-label='😀, grinning']").eq(0).click({force: true}); - }); - - // * Assert that Selected emoji is now displayed next to the board title - cy.get('.IconSelector span').contains('😀'); - - // # Click on the emoji next to the board title - cy.get('.IconSelector .MenuWrapper').should('exist').click({force: true}); - - // * Assert that Dropdown menu with 3 options appears - cy.findByText('Random').should('exist'); - cy.findByText('Pick icon').should('exist'); - cy.findByText('Remove icon').should('exist'); - - // # Click "Remove icon" - cy.findByText('Remove icon').click({force: true}); - - // * Assert that Icon next to the board title is removed - cy.get('.IconSelector').should('not.exist'); - }); - - it('MM-T5397 Can create and delete a board and a card', () => { - // Visit a page and create new empty board - cy.visit('/boards'); - cy.uiCreateEmptyBoard(); - - // Change board title - cy.log('**Change board title**'); - cy.get('.Editable.title'). - type(boardTitle). - type('{enter}'). - should('have.value', boardTitle); - - // Rename board view - cy.log('**Rename board view**'); - const boardViewTitle = `Test board (${timestamp})`; - cy.get(".ViewHeader>.viewSelector>.Editable[title='Board view']").should('exist'); - cy.get('.ViewHeader>.viewSelector>.Editable'). - clear(). - type(boardViewTitle). - type('{esc}'); - cy.get(`.ViewHeader .Editable[title='${boardViewTitle}']`).should('exist'); - - // Create card - cy.log('**Create card**'); - cy.get('.ViewHeader').contains('New').click(); - cy.get('.CardDetail').should('exist'); - - //Check title has focus when card is created - cy.log('**Check title has focus when card is created**'); - cy.get('.CardDetail .EditableArea.title'). - should('have.focus'); - - // Change card title - cy.log('**Change card title**'); - // eslint-disable-next-line cypress/no-unnecessary-waiting - cy.get('.CardDetail .EditableArea.title'). - click(). - should('have.focus'). - type(cardTitle). - should('have.value', cardTitle); - - // Close card dialog - cy.log('**Close card dialog**'); - cy.get('.Dialog Button[title=\'Close dialog\']'). - should('be.visible'). - click(); - - // Create a card by clicking on the + button - cy.log('**Create a card by clicking on the + button**'); - cy.get('.KanbanColumnHeader button .AddIcon').click(); - cy.get('.CardDetail').should('exist'); - cy.get('.Dialog.dialog-back .wrapper').click({force: true}); - - // Create table view - cy.log('**Create table view**'); - cy.get('.ViewHeader').get('.DropdownIcon').first().parent().click(); - cy.get('.ViewHeader').contains('Add view').trigger('mouseover'); - cy.get('.ViewHeader'). - contains('Add view'). - parent(). - contains('Table'). - click(); - cy.get(".ViewHeader .Editable[title='Table view']").should('exist'); - cy.get(`.TableRow [value='${cardTitle}']`).should('exist'); - - // Rename table view - cy.log('**Rename table view**'); - const tableViewTitle = `Test table (${timestamp})`; - cy.get(".ViewHeader .Editable[title='Table view']"). - clear(). - type(tableViewTitle). - type('{esc}'); - cy.get(`.ViewHeader .Editable[title='${tableViewTitle}']`).should('exist'); - - // Sort the table - cy.log('**Sort the table**'); - cy.get('.ViewHeader').contains('Sort').click(); - cy.get('.ViewHeader'). - contains('Sort'). - parent(). - contains('Name'). - click(); - - // Delete board - cy.log('**Delete board**'); - cy.get('.Sidebar .octo-sidebar-list').then((el) => { - cy.log(el.text()); - }); - cy.get('.Sidebar .octo-sidebar-list'). - contains(boardTitle). - parent(). - find('.MenuWrapper'). - find('button.IconButton'). - click({force: true}); - cy.contains('Delete board').click({force: true}); - cy.get('.DeleteBoardDialog button.danger').click({force: true}); - cy.contains(boardTitle).should('not.exist'); - }); - - it('MM-T4433 Scrolls the kanban board when dragging card to edge', () => { - // Visit a page and create new empty board - cy.visit('/boards'); - cy.uiCreateEmptyBoard(); - - // Create 10 empty groups - cy.log('**Create new empty groups**'); - for (let i = 0; i < 10; i++) { - cy.contains('+ Add a group').scrollIntoView().should('be.visible').click(); - cy.get('.KanbanColumnHeader .Editable[value=\'New group\']').should('have.length', i + 1); - } - - // Create empty card in last group - cy.log('**Create new empty card in first group**'); - cy.get('.octo-board-column').last().contains('+ New').scrollIntoView().click(); - cy.get('.Dialog').should('exist'); - cy.get('.Dialog Button[title=\'Close dialog\']').should('be.visible').click(); - cy.get('.KanbanCard').scrollIntoView().should('exist'); - - // Drag card to right corner and expect scroll to occur - cy.get('.Kanban').invoke('scrollLeft').should('not.equal', 0); - - cy.get('.KanbanCard'). - trigger('dragstart'); - - // wait necessary to trigger scroll animation for some time - // eslint-disable-next-line cypress/no-unnecessary-waiting - cy.get('.Kanban'). - trigger('dragover', {clientX: 400, clientY: Cypress.config().viewportHeight / 2}). - wait(timeouts.TEN_SEC). - trigger('dragend'); - - cy.get('.Kanban').invoke('scrollLeft').should('equal', 0); - }); - - it('MM-T5398 cut/undo/redo work in comments', () => { - const isMAC = navigator.userAgent.indexOf('Mac') !== -1; - const ctrlKey = isMAC ? 'meta' : 'ctrl'; - - // Visit a page and create new empty board - cy.visit('/boards'); - cy.uiCreateEmptyBoard(); - - // Create card - cy.log('**Create card**'); - cy.get('.ViewHeader').contains('New').click(); - cy.get('.CardDetail').should('exist'); - - cy.log('**Add comment**'); - cy.get('.CommentsList'). - should('exist'). - findAllByTestId('preview-element'). - click(); - - cy.get('.CommentsList .MarkdownEditor'). - type('Test Text'); - - cy.log('**Cut comment**'); - cy.get('.CommentsList .MarkdownEditor'). - type('{selectAll}'). - trigger('cut'). - should('have.text', ''); - - cy.log('**Undo comment**'); - cy.get('.CommentsList .MarkdownEditor'). - type(`{${ctrlKey}+z}`). - should('have.text', 'Test Text'); - - cy.log('**Redo comment**'); - cy.get('.CommentsList .MarkdownEditor'). - type(`{shift+${ctrlKey}+z}`). - should('have.text', ''); - }); -}); diff --git a/e2e-tests/cypress/tests/integration/boards/group_by_property_spec.ts b/e2e-tests/cypress/tests/integration/boards/group_by_property_spec.ts deleted file mode 100644 index a63f43a3ebe..00000000000 --- a/e2e-tests/cypress/tests/integration/boards/group_by_property_spec.ts +++ /dev/null @@ -1,68 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -// *************************************************************** -// - [#] indicates a test step (e.g. # Go to a page) -// - [*] indicates an assertion (e.g. * Check the title) -// - Use element ID when selecting an element. Create one if none. -// *************************************************************** - -// Stage: @prod -// Group: @boards - -describe('Group board by different properties', () => { - before(() => { - // # Login as new user - cy.apiInitSetup({loginAfter: true}); - cy.clearLocalStorage(); - }); - - it('MM-T4291 Group by different property', () => { - cy.visit('/boards'); - - // Create new board - cy.uiCreateNewBoard('Testing'); - - // Add a new group - cy.uiAddNewGroup('Group 1'); - - // Add a new card to the group - cy.log('**Add a new card to the group**'); - cy.findAllByRole('button', {name: '+ New'}).eq(1).click(); - cy.findByRole('dialog').should('exist'); - cy.findByTestId('select-non-editable').findByText('Group 1').should('exist'); - cy.get('#mainBoardBody').findByText('Untitled').should('exist'); - - // Add new select property - cy.log('**Add new select property**'); - cy.findAllByRole('button', {name: '+ Add a property'}).click(); - cy.findAllByRole('button', {name: 'Select'}).click(); - cy.findByRole('textbox', {name: 'Select'}).type('{enter}'); - cy.findByRole('dialog').findByRole('button', {name: 'Select'}).should('exist'); - - // Close card dialog - cy.log('**Close card dialog**'); - cy.findByRole('button', {name: 'Close dialog'}).should('exist').click(); - - cy.findByRole('dialog').should('not.exist'); - - // Group by new select property - cy.log('**Group by new select property**'); - cy.findByRole('button', {name: /Group by:/}).click(); - cy.findByRole('button', {name: 'Status'}).get('.CheckIcon').should('exist'); - cy.findByRole('button', {name: 'Select'}).click(); - cy.findByTitle(/empty Select property/).contains('No Select'); - cy.get('#mainBoardBody').findByText('Untitled').should('exist'); - - // Add another new group - cy.log('**Add another new group**'); - cy.findByRole('button', {name: '+ Add a group'}).click(); - cy.findByRole('textbox', {name: 'New group'}).should('exist'); - - // Add a new card to another group - cy.log('**Add a new card to another group**'); - cy.findAllByRole('button', {name: '+ New'}).eq(1).click(); - cy.findByRole('dialog').should('exist'); - cy.findAllByTestId('select-non-editable').last().findByText('New group').should('exist'); - }); -}); diff --git a/e2e-tests/cypress/tests/integration/boards/manage_groups_spec.ts b/e2e-tests/cypress/tests/integration/boards/manage_groups_spec.ts deleted file mode 100644 index 12b575ae572..00000000000 --- a/e2e-tests/cypress/tests/integration/boards/manage_groups_spec.ts +++ /dev/null @@ -1,104 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -// *************************************************************** -// - [#] indicates a test step (e.g. # Go to a page) -// - [*] indicates an assertion (e.g. * Check the title) -// - Use element ID when selecting an element. Create one if none. -// *************************************************************** - -// Stage: @prod -// Group: @boards - -describe('Manage groups', () => { - beforeEach(() => { - // # Login as new user - cy.apiAdminLogin().apiInitSetup({loginAfter: true}); - cy.clearLocalStorage(); - }); - - it('MM-T4284 Adding a group', () => { - // Visit a page and create new empty board - cy.visit('/boards'); - cy.uiCreateEmptyBoard(); - - cy.contains('+ Add a group').click({force: true}); - cy.get('.KanbanColumnHeader .Editable[value=\'New group\']').should('exist'); - - cy.get('.KanbanColumnHeader .Editable[value=\'New group\']'). - clear(). - type('Group 1'). - blur(); - cy.get('.KanbanColumnHeader .Editable[value=\'Group 1\']').should('exist'); - }); - - it('MM-T4285 Adding group color', () => { - // Visit a page and create new empty board - cy.visit('/boards'); - cy.uiCreateEmptyBoard(); - - cy.contains('+ Add a group').click({force: true}); - cy.get('.KanbanColumnHeader .Editable[value=\'New group\']').should('exist'); - - cy.get('.KanbanColumnHeader').last().within(() => { - cy.get('.icon-dots-horizontal').click({force: true}); - cy.get('.menu-options').should('exist').first().within(() => { - cy.contains('Hide').should('exist'); - cy.contains('Delete').should('exist'); - - // Some colours - cy.contains('Brown').should('exist'); - cy.contains('Gray').should('exist'); - cy.contains('Orange').should('exist'); - - // Click on green - cy.contains('Green').should('be.visible').click(); - }); - }); - - cy.get('.KanbanColumnHeader').last().within(() => { - cy.get('.Label.propColorGreen').should('exist'); - }); - }); - - it('MM-T4287 Hiding/unhiding a group', () => { - // Step 1: Create an empty board and add a group - cy.visit('/boards'); - cy.uiCreateEmptyBoard(); - - cy.contains('+ Add a group').click({force: true}); - cy.get('.KanbanColumnHeader .Editable[value=\'New group\']').should('exist'); - - cy.get('.KanbanColumnHeader .Editable[value=\'New group\']'). - clear(). - type('Group 1'). - blur(); - - cy.get('.KanbanColumnHeader .Editable[value=\'Group 1\']').should('exist'); - - // Step 2: Click on the three dots next to "Group 1" - cy.get('.KanbanColumnHeader').last().within(() => { - cy.get('.icon-dots-horizontal').click({force: true}); - cy.get('.menu-options').should('exist').first().within(() => { - cy.contains('Hide').should('exist'); - cy.contains('Delete').should('exist'); - - // Some colours - cy.contains('Brown').should('exist'); - cy.contains('Gray').should('exist'); - cy.contains('Orange').should('exist'); - }); - }); - - // Step 3: Click on "Hide" - cy.contains('Hide').click({force: true}); - cy.get('.octo-board-hidden-item').contains('Group 1').should('exist'); - cy.get('.KanbanColumnHeader .Editable[value=\'Group 1\']').should('not.exist'); - - // Step 4: Click "Group 1", then click "Show" in the dropdown - cy.contains('Group 1').click({force: true}); - cy.contains('Show').click({force: true}); - cy.get('.octo-board-hidden-item').should('not.exist'); - cy.get('.KanbanColumnHeader .Editable[value=\'Group 1\']').should('exist'); - }); -}); diff --git a/e2e-tests/cypress/tests/integration/channels/accessibility/accessibility_account_settings_spec.js b/e2e-tests/cypress/tests/integration/channels/accessibility/accessibility_account_settings_spec.js index c7ea056a693..94a44a509e9 100644 --- a/e2e-tests/cypress/tests/integration/channels/accessibility/accessibility_account_settings_spec.js +++ b/e2e-tests/cypress/tests/integration/channels/accessibility/accessibility_account_settings_spec.js @@ -34,7 +34,7 @@ describe('Verify Accessibility Support in different sections in Settings and Pro {key: 'desktop', label: 'Desktop Notifications', type: 'radio'}, {key: 'email', label: 'Email Notifications', type: 'radio'}, {key: 'push', label: 'Mobile Push Notifications', type: 'radio'}, - {key: 'keys', label: 'Words That Trigger Mentions', type: 'checkbox'}, + {key: 'keysWithNotification', label: 'Keywords that trigger Notifications', type: 'checkbox'}, {key: 'comments', label: 'Reply notifications', type: 'radio'}, ], display: [ diff --git a/e2e-tests/cypress/tests/integration/channels/auth_sso/authentication_4_spec.ts b/e2e-tests/cypress/tests/integration/channels/auth_sso/authentication_4_spec.ts index 4e8a3057b47..ea0d702e441 100644 --- a/e2e-tests/cypress/tests/integration/channels/auth_sso/authentication_4_spec.ts +++ b/e2e-tests/cypress/tests/integration/channels/auth_sso/authentication_4_spec.ts @@ -130,10 +130,10 @@ describe('Authentication', () => { cy.get('.admin-console__header').should('be.visible').and('have.text', 'Password'); cy.findByTestId('passwordMinimumLengthinput').should('be.visible').and('have.value', '8'); - cy.findByLabelText('At least one lowercase letter').get('input').should('not.be.checked'); - cy.findByLabelText('At least one uppercase letter').get('input').should('not.be.checked'); - cy.findByLabelText('At least one number').get('input').should('not.be.checked'); - cy.findByLabelText('At least one symbol (e.g. "~!@#$%^&*()")').get('input').should('not.be.checked'); + cy.findByText('At least one lowercase letter').siblings().should('not.be.checked'); + cy.findByText('At least one uppercase letter').siblings().should('not.be.checked'); + cy.findByText('At least one number').siblings().should('not.be.checked'); + cy.findByText('At least one symbol (e.g. "~!@#$%^&*()")').siblings().should('not.be.checked'); if (!isCloudLicensed) { cy.findByTestId('maximumLoginAttemptsinput').should('be.visible').and('have.value', '10'); diff --git a/e2e-tests/cypress/tests/integration/channels/channel/archived_channels_1_spec.js b/e2e-tests/cypress/tests/integration/channels/channel/archived_channels_1_spec.js index bc48e130f99..0e6124d20f2 100644 --- a/e2e-tests/cypress/tests/integration/channels/channel/archived_channels_1_spec.js +++ b/e2e-tests/cypress/tests/integration/channels/channel/archived_channels_1_spec.js @@ -16,6 +16,7 @@ describe('Leave an archived channel', () => { let testTeam; let offTopicUrl; const channelType = { + all: 'Channel Type: All', public: 'Channel Type: Public', archived: 'Channel Type: Archived', }; @@ -98,19 +99,18 @@ describe('Leave an archived channel', () => { cy.get('#showMoreChannels').click(); // # More channels modal opens - cy.get('#browseChannelsModal').should('be.visible').within(() => { - // # Click on dropdown - cy.findByText(channelType.public).should('be.visible').click(); + cy.get('#browseChannelsModal').should('be.visible'); - // # Click archived channels - cy.findByText('Archived Channels').click(); + // # Click on dropdown + cy.findByText(channelType.all).should('be.visible').click(); - // # Modal should contain created channel - cy.get('#moreChannelsList').should('contain', channel.display_name); - }); + // # Click archived channels + cy.findByText('Archived channels').click(); - cy.get('body').typeWithForce('{esc}'); + // # Modal should contain created channel + cy.get('#moreChannelsList').should('contain', channel.display_name); }); + cy.get('body').typeWithForce('{esc}'); }); it('MM-T1699 - Browse Channels for all channel types shows archived channels option', () => { @@ -146,12 +146,12 @@ describe('Leave an archived channel', () => { cy.get('#showMoreChannels').click(); // # More channels modal opens - cy.get('#browseChannelsModal').should('be.visible').within(() => { - // # Public channel list opens by default - cy.findByText(channelType.public).should('be.visible').click(); + cy.get('#browseChannelsModal').should('be.visible').then(() => { + // # All channel list opens by default + cy.findByText(channelType.all).should('be.visible').click(); // # Click on archived channels - cy.findByText('Archived Channels').click(); + cy.findByText('Archived channels').click(); // # Channel list should contain newly created channels cy.get('#moreChannelsList').should('contain', archivedPrivateChannel.name); @@ -199,12 +199,12 @@ describe('Leave an archived channel', () => { cy.get('#showMoreChannels').click(); // # More channels modal opens - cy.get('#browseChannelsModal').should('be.visible').within(() => { - // # Public channels are shown by default - cy.findByText(channelType.public).should('be.visible').click(); + cy.get('#browseChannelsModal').should('be.visible').then(() => { + // # All channels are shown by default + cy.findByText(channelType.all).should('be.visible').click(); // # Go to archived channels - cy.findByText('Archived Channels').click(); + cy.findByText('Archived channels').click(); // # Channel list should contain both archived public channels cy.get('#moreChannelsList').should('contain', archivedPublicChannel1.display_name); @@ -253,12 +253,12 @@ describe('Leave an archived channel', () => { cy.get('#showMoreChannels').click(); // # More channels modal opens - cy.get('#browseChannelsModal').should('be.visible').within(() => { - // # Show public channels is visible by default - cy.findByText(channelType.public).should('be.visible').click(); + cy.get('#browseChannelsModal').should('be.visible').then(() => { + // # Show all channels is visible by default + cy.findByText(channelType.all).should('be.visible').click(); // # Go to archived channels - cy.findByText('Archived Channels').click(); + cy.findByText('Archived channels').click(); // # Channel list should contain only the private channel user is a member of cy.get('#moreChannelsList').should('contain', archivedPrivateChannel1.name); @@ -287,12 +287,12 @@ describe('Leave an archived channel', () => { // # Click on browse channels cy.get('#showMoreChannels').click(); - // # More channels modal opens and lands on public channels - cy.get('#browseChannelsModal').should('be.visible').within(() => { - cy.findByText(channelType.public).should('be.visible').click(); + // # More channels modal opens and lands on all channels + cy.get('#browseChannelsModal').should('be.visible').then(() => { + cy.findByText(channelType.all).should('be.visible').click(); // # Go to archived channels - cy.findByText('Archived Channels').click(); + cy.findByText('Archived channels').click(); // # More channels list should contain the archived channel cy.get('#moreChannelsList').should('contain', archivedChannel.display_name); @@ -323,7 +323,8 @@ describe('Leave an archived channel', () => { cy.get('#showMoreChannels').click(); // # Modal should not contain the created channel - cy.get('#channelsMoreDropdown').should('not.exist'); + cy.findByText(channelType.all).should('be.visible').click(); + cy.findByText('Archived channels').should('not.exist'); cy.get('#moreChannelsList').should('not.contain', channel.name); }); cy.get('body').typeWithForce('{esc}'); diff --git a/e2e-tests/cypress/tests/integration/channels/channel/browse_channels_spec.js b/e2e-tests/cypress/tests/integration/channels/channel/browse_channels_spec.js index f50eb4c65de..948f1ec09e5 100644 --- a/e2e-tests/cypress/tests/integration/channels/channel/browse_channels_spec.js +++ b/e2e-tests/cypress/tests/integration/channels/channel/browse_channels_spec.js @@ -15,6 +15,7 @@ import * as TIMEOUTS from '../../../fixtures/timeouts'; import {createPrivateChannel} from '../enterprise/elasticsearch_autocomplete/helpers'; const channelType = { + all: 'Channel Type: All', public: 'Channel Type: Public', archived: 'Channel Type: Archived', }; @@ -69,8 +70,8 @@ describe('Channels', () => { cy.uiBrowseOrCreateChannel('Browse channels').click(); cy.get('#browseChannelsModal').should('be.visible').within(() => { - // * Dropdown should be visible, defaulting to "Public Channels" - cy.get('#channelsMoreDropdown').should('be.visible').and('contain', channelType.public).wait(TIMEOUTS.HALF_SEC); + // * Dropdown should be visible, defaulting to "All Channels" + cy.get('#menuWrapper').should('be.visible').and('contain', channelType.all).wait(TIMEOUTS.HALF_SEC); cy.get('#searchChannelsTextbox').should('be.visible').type(testChannel.display_name).wait(TIMEOUTS.HALF_SEC); cy.get('#moreChannelsList').should('be.visible').children().should('have.length', 1).within(() => { @@ -111,27 +112,27 @@ describe('Channels', () => { // # Go to LHS and click 'Browse channels' cy.uiBrowseOrCreateChannel('Browse channels').click(); - cy.get('#browseChannelsModal').should('be.visible').within(() => { - // # CLick dropdown to open selection - cy.get('#channelsMoreDropdown').should('be.visible').click().within((el) => { - // # Click on archived channels item - cy.findByText('Archived Channels').should('be.visible').click(); + cy.get('#browseChannelsModal').should('be.visible'); - // * Channel test should be visible as an archived channel in the list - cy.wrap(el).should('contain', channelType.archived); - }); + // # CLick dropdown to open selection + cy.get('#menuWrapper').should('be.visible').click(); - cy.get('#searchChannelsTextbox').should('be.visible').type(testChannel.display_name).wait(TIMEOUTS.HALF_SEC); - cy.get('#moreChannelsList').children().should('have.length', 1).within(() => { - cy.findByText(testChannel.display_name).should('be.visible'); - }); - cy.get('#searchChannelsTextbox').clear(); + // # Click on archived channels item + cy.findByText('Archived channels').should('be.visible').click(); - // * Test channel should be visible as a archived channel in the list - cy.get('#moreChannelsList').should('be.visible').within(() => { - // # Click to view archived channel - cy.findByText(testChannel.display_name).scrollIntoView().should('be.visible').click(); - }); + // * Menu text should be updated to reflect the selection + cy.get('#menuWrapper').should('contain', channelType.archived); + + cy.get('#searchChannelsTextbox').should('be.visible').type(testChannel.display_name).wait(TIMEOUTS.HALF_SEC); + cy.get('#moreChannelsList').children().should('have.length', 1).within(() => { + cy.findByText(testChannel.display_name).should('be.visible'); + }); + cy.get('#searchChannelsTextbox').clear(); + + // * Test channel should be visible as a archived channel in the list + cy.get('#moreChannelsList').should('be.visible').within(() => { + // # Click to view archived channel + cy.findByText(testChannel.display_name).scrollIntoView().should('be.visible').click(); }); // * Assert that channel is archived and new messages can't be posted. @@ -177,7 +178,7 @@ describe('Channels', () => { }); }); - it('MM-T1702 Search works when changing public/archived options in the dropdown', () => { + it('MM-T1702 Search works when changing public/all options in the dropdown', () => { cy.apiAdminLogin(); cy.apiUpdateConfig({ TeamSettings: { @@ -231,32 +232,34 @@ describe('Channels', () => { // # Go to LHS and click 'Browse channels' cy.uiBrowseOrCreateChannel('Browse channels').click(); - // * Dropdown should be visible, defaulting to "Public Channels" - cy.get('#channelsMoreDropdown').should('be.visible').within((el) => { - cy.wrap(el).should('contain', channelType.public); + // * Dropdown should be visible, defaulting to "All channels" + cy.get('#menuWrapper').should('be.visible').within((el) => { + cy.wrap(el).should('contain', channelType.all); }); // * Users should be able to type and search cy.get('#searchChannelsTextbox').should('be.visible').type('iv').wait(TIMEOUTS.HALF_SEC); - cy.get('#moreChannelsList').should('be.visible').children().should('have.length', 1).within(() => { + cy.get('#moreChannelsList').should('be.visible').children().should('have.length', 2); + cy.get('#moreChannelsList').should('be.visible').within(() => { cy.findByText(newChannel.display_name).should('be.visible'); }); - cy.get('#browseChannelsModal').should('be.visible').within(() => { - // * Users should be able to switch to "Archived Channels" list - cy.get('#channelsMoreDropdown').should('be.visible').and('contain', channelType.public).click().within((el) => { - // # Click on archived channels item - cy.findByText('Archived Channels').should('be.visible').click(); + cy.get('#browseChannelsModal').should('be.visible'); - // * Modal should show the archived channels list - cy.wrap(el).should('contain', channelType.archived); - }).wait(TIMEOUTS.HALF_SEC); - cy.get('#searchChannelsTextbox').clear(); - cy.get('#moreChannelsList').should('be.visible').children().should('have.length', 2); - cy.get('#moreChannelsList').within(() => { - cy.findByText(testArchivedChannel.display_name).should('be.visible'); - cy.findByText(testPrivateArchivedChannel.display_name).should('be.visible'); - }); + // * Users should be able to switch to "Archived Channels" list + cy.get('#menuWrapper').should('be.visible').and('contain', channelType.all).click().wait(TIMEOUTS.HALF_SEC); + + // # Click on archived channels item + cy.findByText('Archived channels').should('be.visible').click(); + + // * Modal menu should be updated accordingly + cy.get('#menuWrapper').should('contain', channelType.archived); + + cy.get('#searchChannelsTextbox').clear(); + cy.get('#moreChannelsList').should('be.visible').children().should('have.length', 2); + cy.get('#moreChannelsList').within(() => { + cy.findByText(testArchivedChannel.display_name).should('be.visible'); + cy.findByText(testPrivateArchivedChannel.display_name).should('be.visible'); }); }); }); @@ -287,9 +290,10 @@ function verifyBrowseChannelsModal(isEnabled) { // * Verify that the browse channels modal is open and with or without option to view archived channels cy.get('#browseChannelsModal').should('be.visible').within(() => { if (isEnabled) { - cy.get('#channelsMoreDropdown').should('be.visible').and('have.text', channelType.public); + cy.get('#menuWrapper').should('be.visible').and('have.text', channelType.all); } else { - cy.get('#channelsMoreDropdown').should('not.exist'); + cy.get('#menuWrapper').click(); + cy.findByText('Archived channels').should('not.exist'); } }); } diff --git a/e2e-tests/cypress/tests/integration/channels/channel/more_public_channels_spec.js b/e2e-tests/cypress/tests/integration/channels/channel/browse_public_channels_spec.js similarity index 83% rename from e2e-tests/cypress/tests/integration/channels/channel/more_public_channels_spec.js rename to e2e-tests/cypress/tests/integration/channels/channel/browse_public_channels_spec.js index ec150d22bb8..b9b83c06d8d 100644 --- a/e2e-tests/cypress/tests/integration/channels/channel/more_public_channels_spec.js +++ b/e2e-tests/cypress/tests/integration/channels/channel/browse_public_channels_spec.js @@ -14,7 +14,7 @@ function verifyNoChannelToJoinMessage(isVisible) { cy.findByText('No public channels').should(isVisible ? 'be.visible' : 'not.exist'); } -describe('more public channels', () => { +describe('browse public channels', () => { let testUser; let otherUser; let testTeam; @@ -41,7 +41,7 @@ describe('more public channels', () => { }); }); - it('MM-T1664 Channels do not disappear from More Channels modal', () => { + it('MM-T1664 Channels do not disappear from Browse Channels modal', () => { // # Login as other user cy.apiLogin(otherUser); @@ -51,8 +51,14 @@ describe('more public channels', () => { // # Go to LHS and click 'Browse channels' cy.uiBrowseOrCreateChannel('Browse channels').click(); - // * Assert that the moreChannelsModel is visible - cy.findByRole('dialog', {name: 'Browse Channels'}).should('be.visible').within(() => { + // * Assert that the browse channel modal is visible + cy.findByRole('dialog', {name: 'Browse Channels'}).should('be.visible').then(() => { + // # Click on dropdown + cy.findByText('Channel Type: All').should('be.visible').click(); + + // # Click archived channels + cy.findByText('Public channels').should('be.visible').click(); + // # Click hide joined checkbox if not already checked cy.findByText('Hide Joined').should('be.visible').then(($checkbox) => { if (!$checkbox.prop('checked')) { @@ -92,7 +98,13 @@ describe('more public channels', () => { cy.uiBrowseOrCreateChannel('Browse channels').click(); // * Assert the moreChannelsModel is visible - cy.findByRole('dialog', {name: 'Browse Channels'}).should('be.visible').within(() => { + cy.findByRole('dialog', {name: 'Browse Channels'}).should('be.visible').then(() => { + // # Click on dropdown + cy.findByText('Channel Type: All').should('be.visible').click(); + + // # Click archived channels + cy.findByText('Public channels').should('be.visible').click(); + // * Assert that the "No more channels to join" message is visible verifyNoChannelToJoinMessage(true); }); diff --git a/e2e-tests/cypress/tests/integration/channels/channel/leave_and_archive_channel_destructive_spec.ts b/e2e-tests/cypress/tests/integration/channels/channel/leave_and_archive_channel_destructive_spec.ts index 2a84722131b..d809c6b47e6 100644 --- a/e2e-tests/cypress/tests/integration/channels/channel/leave_and_archive_channel_destructive_spec.ts +++ b/e2e-tests/cypress/tests/integration/channels/channel/leave_and_archive_channel_destructive_spec.ts @@ -1,8 +1,6 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import {UserProfile} from 'mattermost-redux/types/users'; - // *************************************************************** // - [#] indicates a test step (e.g. # Go to a page) // - [*] indicates an assertion (e.g. * Check the title) @@ -13,7 +11,7 @@ import {UserProfile} from 'mattermost-redux/types/users'; // Group: @channels @channel describe('Leave and Archive channel actions display as destructive', () => { - let testUser: UserProfile; + let testUser: Cypress.UserProfile; let offTopicUrl: string; before(() => { diff --git a/e2e-tests/cypress/tests/integration/channels/channel/leave_channel_spec.js b/e2e-tests/cypress/tests/integration/channels/channel/leave_channel_spec.js index 154d3308fc9..c5a9cefceeb 100644 --- a/e2e-tests/cypress/tests/integration/channels/channel/leave_channel_spec.js +++ b/e2e-tests/cypress/tests/integration/channels/channel/leave_channel_spec.js @@ -30,6 +30,13 @@ describe('Leave channel', () => { testUser = user; testTeam = team; + cy.apiSaveUserPreference([{ + user_id: user.id, + category: 'crt_thread_pane_step', + name: user.id, + value: '0', + }], user.id); + cy.apiLogin(testUser); cy.apiCreateChannel(testTeam.id, 'channel', 'channel').then(({channel}) => { testChannel = channel; diff --git a/e2e-tests/cypress/tests/integration/channels/channel/new_channel_with_board_spec.js b/e2e-tests/cypress/tests/integration/channels/channel/new_channel_with_board_spec.js deleted file mode 100644 index 76deb4ed446..00000000000 --- a/e2e-tests/cypress/tests/integration/channels/channel/new_channel_with_board_spec.js +++ /dev/null @@ -1,49 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -// *************************************************************** -// - [#] indicates a test step (e.g. # Go to a page) -// - [*] indicates an assertion (e.g. * Check the title) -// - Use element ID when selecting an element. Create one if none. -// *************************************************************** - -// Stage: @prod -// Group: @channels @channel - -describe('New Channel modal with Boards enabled', () => { - let testTeam; - - before(() => { - cy.apiInitSetup().then(({team}) => { - testTeam = team; - }); - - cy.apiCreateCustomAdmin().then(({sysadmin}) => { - cy.apiLogin(sysadmin); - cy.visit(`/${testTeam.name}/channels/town-square`); - }); - }); - - it('MM-T5141 New Channel is created with an associated Board', () => { - // # Create new channel with board - const channelName = 'Test Channel With Board'; - cy.uiCreateChannel({ - prefix: 'channel-', - isPrivate: false, - purpose: '', - name: channelName, - createBoard: 'Roadmap', - }).then(() => { - // * Verify that new channel is in the sidebar and is active - cy.url().should('include', `/${testTeam.name}/channels/test-channel`); - cy.get('#channelHeaderTitle').should('contain', channelName); - cy.get(`.SidebarChannel.active:contains(${channelName})`).should('be.visible'); - - // * Verify the board is created - check the message sent - cy.waitUntil(() => cy.getLastPost().then((el) => { - const postedMessageEl = el.find('.post-message__text > p')[0]; - return Boolean(postedMessageEl && postedMessageEl.textContent.includes('linked the board')); - })); - }); - }); -}); diff --git a/e2e-tests/cypress/tests/integration/channels/channel_settings/add_users_to_channel_spec.ts b/e2e-tests/cypress/tests/integration/channels/channel_settings/add_users_to_channel_spec.ts index 13001c8d9f2..544a82b96fc 100644 --- a/e2e-tests/cypress/tests/integration/channels/channel_settings/add_users_to_channel_spec.ts +++ b/e2e-tests/cypress/tests/integration/channels/channel_settings/add_users_to_channel_spec.ts @@ -10,29 +10,53 @@ // Stage: @prod // Group: @channels @channel @channel_settings @smoke +import {getRandomId} from '../../../utils'; + describe('Channel Settings', () => { let testTeam: Cypress.Team; let firstUser: Cypress.UserProfile; let addedUsersChannel: Cypress.Channel; let username: string; + let groupId: string; const usernames: string[] = []; + const users: Cypress.UserProfile[] = []; + before(() => { cy.apiInitSetup().then(({team, user}) => { testTeam = team; firstUser = user; + const teamId = testTeam.id; - // # Add 4 users - for (let i = 0; i < 4; i++) { - cy.apiCreateUser().then(({user: newUser}) => { // eslint-disable-line - cy.apiAddUserToTeam(testTeam.id, newUser.id); + // # Add 10 users + for (let i = 0; i < 10; i++) { + cy.apiCreateUser().then(({user: newUser}) => { + users.push(newUser); + cy.apiAddUserToTeam(teamId, newUser.id); }); } - cy.apiCreateChannel(testTeam.id, 'channel-test', 'Channel').then(({channel}) => { + cy.apiCreateChannel(teamId, 'channel-test', 'Channel').then(({channel}) => { addedUsersChannel = channel; }); + // # Change permission so that regular users can't add team members + cy.apiGetRolesByNames(['team_user']).then((result: any) => { + if (result.roles) { + const role = result.roles[0]; + const permissions = role.permissions.filter((permission) => { + return !(['add_user_to_team'].includes(permission)); + }); + + if (permissions.length !== role.permissions) { + cy.apiPatchRole(role.id, {permissions}); + } + } + }); + cy.apiLogin(firstUser); + }).then(() => { + groupId = getRandomId(); + cy.apiCreateCustomUserGroup(`group${groupId}`, `group${groupId}`, [users[0].id, users[1].id]); }); }); @@ -42,7 +66,7 @@ describe('Channel Settings', () => { cy.visit(`/${testTeam.name}/channels/${channel.name}`); // # Add users to channel - addNumberOfUsersToChannel(1); + addNumberOfUsersToChannel(1, false); cy.getLastPostId().then((id) => { // * The system message should contain 'added to the channel by you' @@ -59,7 +83,7 @@ describe('Channel Settings', () => { cy.apiCreateChannel(testTeam.id, 'channel-test', 'Channel').then(({channel}) => { cy.visit(`/${testTeam.name}/channels/${channel.name}`); - addNumberOfUsersToChannel(3); + addNumberOfUsersToChannel(3, false); cy.getLastPostId().then((id) => { cy.get(`#postMessageText_${id}`).should('contain', '2 others were added to the channel by you'); @@ -82,10 +106,10 @@ describe('Channel Settings', () => { cy.get('#addUsersToChannelModal').should('be.visible'); // # Type into the input box to search for a user - cy.get('#selectItems input').typeWithForce('u'); + cy.get('#selectItems input').typeWithForce('user'); // # First add one user in order to see them disappearing from the list - cy.get('#multiSelectList > div').first().then((el) => { + cy.get('#multiSelectList > div').not(':contains("Already in channel")').first().then((el) => { const childNodes = Array.from(el[0].childNodes); childNodes.map((child: HTMLElement) => usernames.push(child.innerText)); @@ -113,7 +137,7 @@ describe('Channel Settings', () => { }); // Add two more users - addNumberOfUsersToChannel(2); + addNumberOfUsersToChannel(2, false); // Verify that the system post reflects the number of added users cy.getLastPostId().then((id) => { @@ -148,6 +172,179 @@ describe('Channel Settings', () => { }); cy.get('body').type('{esc}'); }); + + it('Add group members to channel', () => { + cy.apiLogin(firstUser); + + // # Create a new channel + cy.apiCreateChannel(testTeam.id, 'new-channel', 'New Channel').then(({channel}) => { + // # Visit the channel + cy.visit(`/${testTeam.name}/channels/${channel.name}`); + + // # Open channel menu and click 'Add Members' + cy.uiOpenChannelMenu('Add Members'); + + // * Assert that modal appears + cy.get('#addUsersToChannelModal').should('be.visible'); + + // # Type 'group'+ id created in beforeAll into the input box + cy.get('#selectItems input').typeWithForce(`group${groupId}`); + + // # Click the first row for a number of times + // cy.get('#multiSelectList').should('be.visible').first().click(); + cy.get('#multiSelectList').should('exist').children().first().click(); + + // # Click the button "Add" to add user to a channel + cy.uiGetButton('Add').click(); + + // # Wait for the modal to disappear + cy.get('#addUsersToChannelModal').should('not.exist'); + + cy.getLastPostId().then((id) => { + // * The system message should contain 'added to the channel by you' + cy.get(`#postMessageText_${id}`).should('contain', 'added to the channel by you'); + + // # Verify username link + verifyMentionedUserAndProfilePopover(id); + }); + + // * Check that the number of channel members is 3 + cy.get('#channelMemberCountText'). + should('be.visible'). + and('have.text', '3'); + }); + }); + + it('Add group members that are not team members', () => { + cy.apiAdminLogin(); + + // # Create a new user + cy.apiCreateUser().then(({user: newUser}) => { + const id = getRandomId(); + + // # Create a custom user group + cy.apiCreateCustomUserGroup(`newgroup${id}`, `newgroup${id}`, [newUser.id]).then(() => { + // # Create a new channel + cy.apiCreateChannel(testTeam.id, 'new-group-channel', 'New Group Channel').then(({channel}) => { + // # Visit a channel + cy.visit(`/${testTeam.name}/channels/${channel.name}`); + + // # Open channel menu and click 'Add Members' + cy.uiOpenChannelMenu('Add Members'); + + // * Assert that modal appears + cy.get('#addUsersToChannelModal').should('be.visible'); + + // # Type 'group' into the input box + cy.get('#selectItems input').typeWithForce(`newgroup${id}`); + + // # Click the first row for a number of times + // cy.get('#multiSelectList').should('be.visible').first().click(); + cy.get('#multiSelectList').should('exist').children().first().click(); + + // * Check you get a warning when adding a non team member + cy.findByTestId('teamWarningBanner').should('contain', '1 user was not selected because they are not a part of this team'); + + // * Check the correct username is appearing in the team invite banner + cy.findByTestId('teamWarningBanner').should('contain', `@${newUser.username}`); + + // # Click the button "Add" to add user to a channel + cy.uiGetButton('Cancel').click(); + + // # Wait for the modal to disappear + cy.get('#addUsersToChannelModal').should('not.exist'); + }); + }); + }); + }); + + it('Add group members and guests that are not team members', () => { + cy.apiAdminLogin(); + + // # Create a new user + cy.apiCreateUser().then(({user: newUser}) => { + // # Create a guest user + cy.apiCreateGuestUser({}).then(({guest}) => { + const id = getRandomId(); + + // # Create a custom user group + cy.apiCreateCustomUserGroup(`guestgroup${id}`, `guestgroup${id}`, [guest.id, newUser.id]).then(() => { + // # Create a new channel + cy.apiCreateChannel(testTeam.id, 'group-guest-channel', 'Channel').then(({channel}) => { + // # Visit a channel + cy.visit(`/${testTeam.name}/channels/${channel.name}`); + + // # Open channel menu and click 'Add Members' + cy.uiOpenChannelMenu('Add Members'); + + // * Assert that modal appears + cy.get('#addUsersToChannelModal').should('be.visible'); + + // # Type 'group' into the input box + cy.get('#selectItems input').typeWithForce(`guestgroup${id}`); + + // # Click the first row for a number of times + // cy.get('#multiSelectList').should('be.visible').first().click(); + cy.get('#multiSelectList').should('exist').children().first().click(); + + // * Check you get a warning when adding a non team member + cy.findByTestId('teamWarningBanner').should('contain', '2 users were not selected because they are not a part of this team'); + + // * Check the correct username is appearing in the invite to team portion + cy.findByTestId('teamWarningBanner').should('contain', `@${newUser.username}`); + + // * Check the guest username is in the warning message and won't be added to the team + cy.findByTestId('teamWarningBanner').should('contain', `@${guest.username} is a guest user`); + + // # Click the button "Add" to add user to a channel + cy.uiGetButton('Cancel').click(); + + // # Wait for the modal to disappear + cy.get('#addUsersToChannelModal').should('not.exist'); + }); + }); + }); + }); + }); + + it('User doesn\'t have permission to add user to team', () => { + cy.apiAdminLogin(); + + // # Create a new user + cy.apiCreateUser().then(({user: newUser}) => { + const id = getRandomId(); + + // # Create a custom user group + cy.apiCreateCustomUserGroup(`newgroup${id}`, `newgroup${id}`, [newUser.id]).then(() => { + // # Create a new channel + cy.apiCreateChannel(testTeam.id, 'new-group-channel', 'Channel').then(({channel}) => { + cy.apiLogin(firstUser); + + // # Visit a channel + cy.visit(`/${testTeam.name}/channels/${channel.name}`); + + // # Open channel menu and click 'Add Members' + cy.uiOpenChannelMenu('Add Members'); + + // * Assert that modal appears + cy.get('#addUsersToChannelModal').should('be.visible'); + + // # Type 'group' into the input box + cy.get('#selectItems input').typeWithForce(`newgroup${id}`); + + // # Click the first row for a number of times + // cy.get('#multiSelectList').should('be.visible').first().click(); + cy.get('#multiSelectList').should('exist').children().first().click(); + + // * Check you get a warning when adding a non team member + cy.findByTestId('teamWarningBanner').should('contain', '1 user was not selected because they are not a part of this team'); + + // * Check the correct username is appearing in the team invite banner + cy.findByTestId('teamWarningBanner').should('contain', `@${newUser.username}`); + }); + }); + }); + }); }); function verifyMentionedUserAndProfilePopover(postId: string) { @@ -169,7 +366,7 @@ function verifyMentionedUserAndProfilePopover(postId: string) { }); } -function addNumberOfUsersToChannel(num = 1) { +function addNumberOfUsersToChannel(num = 1, allowExisting = false) { // # Open channel menu and click 'Add Members' cy.uiOpenChannelMenu('Add Members'); cy.get('#addUsersToChannelModal').should('be.visible'); @@ -177,8 +374,14 @@ function addNumberOfUsersToChannel(num = 1) { // * Assert that modal appears // # Click the first row for a number of times Cypress._.times(num, () => { - cy.get('#selectItems input').typeWithForce('u'); - cy.get('#multiSelectList').should('be.visible').first().click(); + cy.get('#selectItems input').typeWithForce('user'); + + // cy.get('#multiSelectList').should('be.visible').first().click(); + if (allowExisting) { + cy.get('#multiSelectList').should('exist').children().first().click(); + } else { + cy.get('#multiSelectList').should('exist').children().not(':contains("Already in channel")').first().click(); + } }); // # Click the button "Add" to add user to a channel diff --git a/e2e-tests/cypress/tests/integration/channels/collapsed_reply_threads/crt_tour_spec.js b/e2e-tests/cypress/tests/integration/channels/collapsed_reply_threads/crt_tour_spec.js index ea99a56466b..cbcd3181b97 100644 --- a/e2e-tests/cypress/tests/integration/channels/collapsed_reply_threads/crt_tour_spec.js +++ b/e2e-tests/cypress/tests/integration/channels/collapsed_reply_threads/crt_tour_spec.js @@ -25,10 +25,22 @@ describe('Collapsed Reply Threads', () => { }); // # Log in as user that hasn't had CRT enabled before - cy.apiInitSetup({loginAfter: true, promoteNewUserAsAdmin: true, userPrefix: 'tipbutton'}).then(({team, channel}) => { + cy.apiInitSetup({loginAfter: true, promoteNewUserAsAdmin: true, userPrefix: 'tipbutton'}).then(({team, channel, user}) => { testTeam = team; testChannel = channel; + cy.apiSaveUserPreference([{ + user_id: user.id, + category: 'crt_thread_pane_step', + name: user.id, + value: '0', + }, { + user_id: user.id, + category: 'crt_tutorial_triggered', + name: user.id, + value: '0', + }], user.id); + cy.apiCreateUser({prefix: 'other'}).then(({user: user1}) => { otherUser = user1; diff --git a/e2e-tests/cypress/tests/integration/channels/enterprise/accessibility/accessibility_modals_dialogs_spec.js b/e2e-tests/cypress/tests/integration/channels/enterprise/accessibility/accessibility_modals_dialogs_spec.js index 8483be2867e..01fb97f6592 100644 --- a/e2e-tests/cypress/tests/integration/channels/enterprise/accessibility/accessibility_modals_dialogs_spec.js +++ b/e2e-tests/cypress/tests/integration/channels/enterprise/accessibility/accessibility_modals_dialogs_spec.js @@ -86,7 +86,7 @@ describe('Verify Accessibility Support in Modals & Dialogs', () => { }); }); - it('MM-T1467 Accessibility Support in More Channels Dialog screen', () => { + it('MM-T1467 Accessibility Support in Browse Channels Dialog screen', () => { function getChannelAriaLabel(channel) { return channel.display_name.toLowerCase() + ', ' + channel.purpose.toLowerCase(); } @@ -117,8 +117,8 @@ describe('Verify Accessibility Support in Modals & Dialogs', () => { // # Hide already joined channels cy.findByText('Hide Joined').click(); - // # Focus on the Create Channel button and TAB three time - cy.get('#createNewChannelButton').focus().tab().tab().tab(); + // # Focus on the Create Channel button and TAB four time + cy.get('#createNewChannelButton').focus().tab().tab().tab().tab(); // * Verify channel name is highlighted and reader reads the channel name and channel description cy.get('#moreChannelsList').within(() => { @@ -155,11 +155,11 @@ describe('Verify Accessibility Support in Modals & Dialogs', () => { cy.findByRole('heading', {name: modalName}); // * Verify the accessibility support in search input - cy.findByRole('textbox', {name: 'Search for people'}). + cy.findByRole('textbox', {name: 'Search for people or groups'}). should('have.attr', 'aria-autocomplete', 'list'); // # Search for a text and then check up and down arrow - cy.findByRole('textbox', {name: 'Search for people'}). + cy.findByRole('textbox', {name: 'Search for people or groups'}). typeWithForce('u'). wait(TIMEOUTS.HALF_SEC). typeWithForce('{downarrow}{downarrow}{downarrow}{uparrow}'); @@ -184,7 +184,7 @@ describe('Verify Accessibility Support in Modals & Dialogs', () => { }); // # Search for an invalid text and check if reader can read no results - cy.findByRole('textbox', {name: 'Search for people'}). + cy.findByRole('textbox', {name: 'Search for people or groups'}). typeWithForce('somethingwhichdoesnotexist'). wait(TIMEOUTS.HALF_SEC); diff --git a/e2e-tests/cypress/tests/integration/channels/enterprise/cloud/billing/billing_history_free_trial_spec.ts b/e2e-tests/cypress/tests/integration/channels/enterprise/cloud/billing/billing_history_free_trial_spec.ts index 327d4d0c45c..249950a0d98 100644 --- a/e2e-tests/cypress/tests/integration/channels/enterprise/cloud/billing/billing_history_free_trial_spec.ts +++ b/e2e-tests/cypress/tests/integration/channels/enterprise/cloud/billing/billing_history_free_trial_spec.ts @@ -10,116 +10,6 @@ // Stage: @prod // Group: @channels @cloud_only @cloud_trial -function simulateSubscription() { - cy.intercept('GET', '**/api/v4/cloud/subscription/invoices', { - statusCode: 200, - body: [ - { - id: 'in_1Lz8b0I67GP2qpb43kcuMyFP', - number: '8D53267B-0006', - create_at: 1667263490000, - total: 65, - tax: 0, - status: 'open', - description: '', - period_start: 1664582400000, - period_end: 1667260800000, - subscription_id: 'sub_K0AxuWCDoDD9Qq', - line_items: [ - { - price_id: 'price_1KLUYiI67GP2qpb48DXFukcJ', - total: 65, - quantity: 0.06451612903225806, - price_per_unit: 1000, - description: 'Cloud Professional', - }, - ], - current_product_name: 'Cloud Professional', - }, - { - id: 'in_1LntnKI67GP2qpb4VObu3NgP', - number: '8D53267B-0005', - create_at: 1664584986000, - total: 733, - tax: 0, - status: 'failed', - description: '', - period_start: 1661990400000, - period_end: 1664582400000, - subscription_id: 'sub_K0AxuWCDoDD9Qq', - line_items: [ - { - price_id: 'price_1KLUZ2I67GP2qpb45uTS89eb', - total: 733, - quantity: 0.7333333333333333, - price_per_unit: 999, - description: 'Cloud Professional', - }, - ], - current_product_name: 'Cloud Professional', - }, - { - id: 'in_1LntnKI67GP2qpb4VObu3NgV', - number: '8D53267B-0005', - create_at: 1664584986000, - total: 733, - tax: 0, - status: 'paid', - description: '', - period_start: 1661990400000, - period_end: 1664582400000, - subscription_id: 'sub_K0AxuWCDoDD9Qq', - line_items: [ - { - price_id: 'price_1KLUZ2I67GP2qpb45uTS89eb', - total: 733, - quantity: 0.7333333333333333, - price_per_unit: 999, - description: 'Cloud Professional', - }, - ], - current_product_name: 'Cloud Professional', - }, - ], - }); - cy.intercept('GET', '**/api/v4/cloud/subscription', { - statusCode: 200, - body: { - id: 'sub_test1', - is_free_trial: 'true', - customer_id: '5zqhakmibpgyix9juiwwkpfnmr', - product_id: 'prod_K0AxuWCDoDD9Qq', - seats: 25, - status: 'active', - }, - }); - - cy.intercept('GET', '**/api/v4/cloud/products**', { - statusCode: 200, - body: - [ - { - id: 'prod_LSBESgGXq9KlLj', - sku: 'cloud-starter', - price_per_seat: 0, - name: 'Cloud Free', - }, - { - id: 'prod_K0AxuWCDoDD9Qq', - sku: 'cloud-professional', - price_per_seat: 10, - name: 'Cloud Professional', - }, - { - id: 'prod_Jh6tBLcgWWOOog', - sku: 'cloud-enterprise', - price_per_seat: 30, - name: 'Cloud Enterprise', - }, - ], - }); -} - describe('System Console - Billing History', () => { before(() => { simulateSubscription(); @@ -166,6 +56,116 @@ describe('System Console - Billing History', () => { cy.get('@tableRows').eq(1).find('td').eq(4).find('a').should('have.attr', 'href').and('include', 'invoices/in_1LntnKI67GP2qpb4VObu3NgP/pdf'); cy.get('@tableRows').eq(2).find('td').eq(4).find('a').should('have.attr', 'href').and('include', 'invoices/in_1LntnKI67GP2qpb4VObu3NgV/pdf'); }); + + function simulateSubscription() { + cy.intercept('GET', '**/api/v4/cloud/subscription/invoices', { + statusCode: 200, + body: [ + { + id: 'in_1Lz8b0I67GP2qpb43kcuMyFP', + number: '8D53267B-0006', + create_at: 1667263490000, + total: 65, + tax: 0, + status: 'open', + description: '', + period_start: 1664582400000, + period_end: 1667260800000, + subscription_id: 'sub_K0AxuWCDoDD9Qq', + line_items: [ + { + price_id: 'price_1KLUYiI67GP2qpb48DXFukcJ', + total: 65, + quantity: 0.06451612903225806, + price_per_unit: 1000, + description: 'Cloud Professional', + }, + ], + current_product_name: 'Cloud Professional', + }, + { + id: 'in_1LntnKI67GP2qpb4VObu3NgP', + number: '8D53267B-0005', + create_at: 1664584986000, + total: 733, + tax: 0, + status: 'failed', + description: '', + period_start: 1661990400000, + period_end: 1664582400000, + subscription_id: 'sub_K0AxuWCDoDD9Qq', + line_items: [ + { + price_id: 'price_1KLUZ2I67GP2qpb45uTS89eb', + total: 733, + quantity: 0.7333333333333333, + price_per_unit: 999, + description: 'Cloud Professional', + }, + ], + current_product_name: 'Cloud Professional', + }, + { + id: 'in_1LntnKI67GP2qpb4VObu3NgV', + number: '8D53267B-0005', + create_at: 1664584986000, + total: 733, + tax: 0, + status: 'paid', + description: '', + period_start: 1661990400000, + period_end: 1664582400000, + subscription_id: 'sub_K0AxuWCDoDD9Qq', + line_items: [ + { + price_id: 'price_1KLUZ2I67GP2qpb45uTS89eb', + total: 733, + quantity: 0.7333333333333333, + price_per_unit: 999, + description: 'Cloud Professional', + }, + ], + current_product_name: 'Cloud Professional', + }, + ], + }); + cy.intercept('GET', '**/api/v4/cloud/subscription', { + statusCode: 200, + body: { + id: 'sub_test1', + is_free_trial: 'true', + customer_id: '5zqhakmibpgyix9juiwwkpfnmr', + product_id: 'prod_K0AxuWCDoDD9Qq', + seats: 25, + status: 'active', + }, + }); + + cy.intercept('GET', '**/api/v4/cloud/products**', { + statusCode: 200, + body: + [ + { + id: 'prod_LSBESgGXq9KlLj', + sku: 'cloud-starter', + price_per_seat: 0, + name: 'Cloud Free', + }, + { + id: 'prod_K0AxuWCDoDD9Qq', + sku: 'cloud-professional', + price_per_seat: 10, + name: 'Cloud Professional', + }, + { + id: 'prod_Jh6tBLcgWWOOog', + sku: 'cloud-enterprise', + price_per_seat: 30, + name: 'Cloud Enterprise', + }, + ], + }); + } }); describe('System Console - Empty Billing Screen', () => { diff --git a/e2e-tests/cypress/tests/integration/channels/enterprise/cloud/billing/payment_free_trial_spec.ts b/e2e-tests/cypress/tests/integration/channels/enterprise/cloud/billing/payment_free_trial_spec.ts index 3ba4337e272..1b8a861e15c 100644 --- a/e2e-tests/cypress/tests/integration/channels/enterprise/cloud/billing/payment_free_trial_spec.ts +++ b/e2e-tests/cypress/tests/integration/channels/enterprise/cloud/billing/payment_free_trial_spec.ts @@ -10,45 +10,6 @@ // Stage: @prod // Group: @channels @cloud_only @cloud_trial -function simulateSubscription() { - cy.intercept('GET', '**/api/v4/cloud/subscription', { - statusCode: 200, - body: { - id: 'sub_test1', - is_free_trial: 'true', - customer_id: '5zqhakmibpgyix9juiwwkpfnmr', - product_id: 'prod_Jh6tBLcgWWOOog', - seats: 25, - status: 'active', - }, - }); - - cy.intercept('GET', '**/api/v4/cloud/products**', { - statusCode: 200, - body: - [ - { - id: 'prod_LSBESgGXq9KlLj', - sku: 'cloud-starter', - price_per_seat: 0, - name: 'Cloud Free', - }, - { - id: 'prod_K0AxuWCDoDD9Qq', - sku: 'cloud-professional', - price_per_seat: 10, - name: 'Cloud Professional', - }, - { - id: 'prod_Jh6tBLcgWWOOog', - sku: 'cloud-enterprise', - price_per_seat: 30, - name: 'Cloud Enterprise', - }, - ], - }); -} - describe('System Console - Payment Information section', () => { before(() => { // * Check if server has license for Cloud @@ -116,5 +77,43 @@ describe('System Console - Payment Information section', () => { cy.get('span.savedFeedback__text').should('be.visible').should('have.text', 'Thanks for sharing feedback!'); cy.get('button#feedbackSubmitedDone').should('be.enabled').click(); }); -}); + function simulateSubscription() { + cy.intercept('GET', '**/api/v4/cloud/subscription', { + statusCode: 200, + body: { + id: 'sub_test1', + is_free_trial: 'true', + customer_id: '5zqhakmibpgyix9juiwwkpfnmr', + product_id: 'prod_Jh6tBLcgWWOOog', + seats: 25, + status: 'active', + }, + }); + + cy.intercept('GET', '**/api/v4/cloud/products**', { + statusCode: 200, + body: + [ + { + id: 'prod_LSBESgGXq9KlLj', + sku: 'cloud-starter', + price_per_seat: 0, + name: 'Cloud Free', + }, + { + id: 'prod_K0AxuWCDoDD9Qq', + sku: 'cloud-professional', + price_per_seat: 10, + name: 'Cloud Professional', + }, + { + id: 'prod_Jh6tBLcgWWOOog', + sku: 'cloud-enterprise', + price_per_seat: 30, + name: 'Cloud Enterprise', + }, + ], + }); + } +}); diff --git a/e2e-tests/cypress/tests/integration/channels/enterprise/extend_session/session_length_spec.ts b/e2e-tests/cypress/tests/integration/channels/enterprise/extend_session/session_length_spec.ts index cad52cbb1f7..832cf7fd9d3 100644 --- a/e2e-tests/cypress/tests/integration/channels/enterprise/extend_session/session_length_spec.ts +++ b/e2e-tests/cypress/tests/integration/channels/enterprise/extend_session/session_length_spec.ts @@ -10,37 +10,6 @@ // Stage: @prod // Group: @channels @enterprise @not_cloud @system_console -// # Goes to the System Scheme page as System Admin -const goToSessionLengths = () => { - cy.apiAdminLogin(); - cy.visit('/admin_console/environment/session_lengths'); -}; - -// # Wait's until the Saving text becomes Save -const waitUntilConfigSave = () => { - cy.waitUntil(() => cy.get('#saveSetting').then((el) => { - return el[0].innerText === 'Save'; - })); -}; - -// Clicks the save button in the system console page. -// waitUntilConfigSaved: If we need to wait for the save button to go from saving -> save. -// Usually we need to wait unless we are doing this in team override scheme -const saveConfig = (waitUntilConfigSaved = true, clickConfirmationButton = false) => { - // # Save if possible (if previous test ended abruptly all permissions may already be enabled) - cy.get('#saveSetting').then((btn) => { - if (btn.is(':enabled')) { - btn.click(); - } - }); - if (clickConfirmationButton) { - cy.get('#confirmModalButton').click(); - } - if (waitUntilConfigSaved) { - waitUntilConfigSave(); - } -}; - describe('MM-T2574 Session Lengths', () => { before(() => { cy.shouldNotRunOnCloudEdition(); @@ -125,4 +94,35 @@ describe('MM-T2574 Session Lengths', () => { } }); }); + + // # Goes to the System Scheme page as System Admin + const goToSessionLengths = () => { + cy.apiAdminLogin(); + cy.visit('/admin_console/environment/session_lengths'); + }; + + // # Wait's until the Saving text becomes Save + const waitUntilConfigSave = () => { + cy.waitUntil(() => cy.get('#saveSetting').then((el) => { + return el[0].innerText === 'Save'; + })); + }; + + // Clicks the save button in the system console page. + // waitUntilConfigSaved: If we need to wait for the save button to go from saving -> save. + // Usually we need to wait unless we are doing this in team override scheme + const saveConfig = (waitUntilConfigSaved = true, clickConfirmationButton = false) => { + // # Save if possible (if previous test ended abruptly all permissions may already be enabled) + cy.get('#saveSetting').then((btn) => { + if (btn.is(':enabled')) { + btn.click(); + } + }); + if (clickConfirmationButton) { + cy.get('#confirmModalButton').click(); + } + if (waitUntilConfigSaved) { + waitUntilConfigSave(); + } + }; }); diff --git a/e2e-tests/cypress/tests/integration/channels/enterprise/ldap/ldap_group_sync_spec.js b/e2e-tests/cypress/tests/integration/channels/enterprise/ldap/ldap_group_sync_spec.js index ed73e0f2779..4aa2f4b0471 100644 --- a/e2e-tests/cypress/tests/integration/channels/enterprise/ldap/ldap_group_sync_spec.js +++ b/e2e-tests/cypress/tests/integration/channels/enterprise/ldap/ldap_group_sync_spec.js @@ -232,9 +232,9 @@ context('ldap', () => { cy.visit(`/${testTeam.name}`); cy.uiBrowseOrCreateChannel('Browse channels').click(); - // * Search private channel name and make sure it isn't there in public channel directory + // * Search private channel name and make sure it is still visible cy.get('#searchChannelsTextbox').type(testChannel.display_name); - cy.get('#moreChannelsList').should('include.text', 'No results for'); + cy.get('#moreChannelsList').should('include.text', testChannel.display_name); }); it('MM-T2629 - Private to public - More....', () => { diff --git a/e2e-tests/cypress/tests/integration/channels/enterprise/self-hosted/self_hosted_pricing_modal_spec.ts b/e2e-tests/cypress/tests/integration/channels/enterprise/self-hosted/self_hosted_pricing_modal_spec.ts index 8290bc2ae72..6f9bf36cd0f 100644 --- a/e2e-tests/cypress/tests/integration/channels/enterprise/self-hosted/self_hosted_pricing_modal_spec.ts +++ b/e2e-tests/cypress/tests/integration/channels/enterprise/self-hosted/self_hosted_pricing_modal_spec.ts @@ -9,26 +9,6 @@ // Group: @channels @enterprise @not_cloud -function withTrialBefore(trialed: string) { - cy.intercept('GET', '**/api/v4/trial-license/prev', { - statusCode: 200, - body: { - IsLicensed: trialed, - IsTrial: trialed, - }, - }); -} - -function withTrialLicense(trial: string) { - cy.intercept('GET', '**/api/v4/license/client?format=old', { - statusCode: 200, - body: { - IsLicensed: 'true', - IsTrial: trial, - }, - }); -} - describe('Self hosted Pricing modal', () => { let urlL: string | undefined; let createdUser: Cypress.UserProfile | undefined; @@ -191,4 +171,24 @@ describe('Self hosted Pricing modal', () => { cy.findByText('https://mattermost.com/pl/pricing/#self-hosted').last().should('exist'); }); + + function withTrialBefore(trialed: string) { + cy.intercept('GET', '**/api/v4/trial-license/prev', { + statusCode: 200, + body: { + IsLicensed: trialed, + IsTrial: trialed, + }, + }); + } + + function withTrialLicense(trial: string) { + cy.intercept('GET', '**/api/v4/license/client?format=old', { + statusCode: 200, + body: { + IsLicensed: 'true', + IsTrial: trial, + }, + }); + } }); diff --git a/e2e-tests/cypress/tests/integration/channels/enterprise/system_console/channel_members_spec.js b/e2e-tests/cypress/tests/integration/channels/enterprise/system_console/channel_members_spec.js index 7b78ec869ee..964cfc0845c 100644 --- a/e2e-tests/cypress/tests/integration/channels/enterprise/system_console/channel_members_spec.js +++ b/e2e-tests/cypress/tests/integration/channels/enterprise/system_console/channel_members_spec.js @@ -119,7 +119,7 @@ describe('Channel members test', () => { cy.get('#addChannelMembers').click(); // # Enter user1 and user2 emails - cy.findByRole('textbox', {name: 'Search for people'}).typeWithForce(`${user1.email}{enter}${user2.email}{enter}`); + cy.findByRole('textbox', {name: 'Search for people or groups'}).typeWithForce(`${user1.email}{enter}${user2.email}{enter}`); // # Confirm add the users cy.get('#addUsersToChannelModal #saveItems').click(); diff --git a/e2e-tests/cypress/tests/integration/channels/enterprise/system_console/compliance/compliance_export_ui_spec.js b/e2e-tests/cypress/tests/integration/channels/enterprise/system_console/compliance/compliance_export_ui_spec.js index 04585b0fcc9..009d33de32e 100644 --- a/e2e-tests/cypress/tests/integration/channels/enterprise/system_console/compliance/compliance_export_ui_spec.js +++ b/e2e-tests/cypress/tests/integration/channels/enterprise/system_console/compliance/compliance_export_ui_spec.js @@ -148,7 +148,7 @@ describe('Compliance Export', () => { cy.findByTestId('enableComplianceExportfalse').click(); // * Verify that exported button is disabled - cy.findByRole('button', {name: /run compliance export job now/i}).should('be.disabled'); + cy.findByRole('button', {name: /run compliance export job now/i}).should('be.enabled'); }); it('MM-T1167 - Compliance Export job can be canceled', () => { diff --git a/e2e-tests/cypress/tests/integration/channels/files_and_attachments/disabled_file_upload_spec.js b/e2e-tests/cypress/tests/integration/channels/files_and_attachments/disabled_file_upload_spec.js index 77574bc69a3..917b84ac50d 100644 --- a/e2e-tests/cypress/tests/integration/channels/files_and_attachments/disabled_file_upload_spec.js +++ b/e2e-tests/cypress/tests/integration/channels/files_and_attachments/disabled_file_upload_spec.js @@ -140,6 +140,7 @@ describe('Upload Files - Settings', () => { }, }], types: [], + getData: () => {}, }}); // * An error should be visible saying 'File attachments are disabled' @@ -165,6 +166,7 @@ describe('Upload Files - Settings', () => { }, }], types: [], + getData: () => {}, }}); // * An error should be visible saying 'File attachments are disabled' diff --git a/e2e-tests/cypress/tests/integration/channels/files_and_attachments/paste_image_spec.js b/e2e-tests/cypress/tests/integration/channels/files_and_attachments/paste_image_spec.js index bb2596f5de7..5fc68aef9f3 100644 --- a/e2e-tests/cypress/tests/integration/channels/files_and_attachments/paste_image_spec.js +++ b/e2e-tests/cypress/tests/integration/channels/files_and_attachments/paste_image_spec.js @@ -41,6 +41,7 @@ describe('Paste Image', () => { }, }], types: [], + getData: () => {}, }}); cy.uiWaitForFileUploadPreview(); diff --git a/e2e-tests/cypress/tests/integration/channels/interactive_dialog/scrollable_spec.js b/e2e-tests/cypress/tests/integration/channels/interactive_dialog/scrollable_spec.js index f54234a9971..7fa398a51f3 100644 --- a/e2e-tests/cypress/tests/integration/channels/interactive_dialog/scrollable_spec.js +++ b/e2e-tests/cypress/tests/integration/channels/interactive_dialog/scrollable_spec.js @@ -84,16 +84,18 @@ describe('Interactive Dialog', () => { if (index === 0) { expect(element.name).to.equal('someuserselector'); cy.wrap($elForm).find('.suggestion-list__item').first().should('be.visible'); - cy.wrap($elForm).find('.form-control').type('{downarrow}'.repeat(10)); + cy.wrap($elForm).find('.form-control').type('{uparrow}', {force: true}); + cy.wrap($elForm).find('.form-control').type('{downarrow}'.repeat(10), {force: true}); cy.wrap($elForm).find('.suggestion-list__item').first().should('not.be.visible'); - cy.wrap($elForm).find('.form-control').type('{uparrow}'.repeat(10)); + cy.wrap($elForm).find('.form-control').type('{uparrow}'.repeat(10), {force: true}); cy.wrap($elForm).find('.suggestion-list__item').first().should('be.visible'); } else if (index === 1) { expect(element.name).to.equal('somechannelselector'); cy.wrap($elForm).find('.suggestion-list__item').first().should('be.visible'); - cy.wrap($elForm).find('.form-control').type('{downarrow}'.repeat(10)); + cy.wrap($elForm).find('.form-control').type('{uparrow}', {force: true}); + cy.wrap($elForm).find('.form-control').type('{downarrow}'.repeat(10), {force: true}); cy.wrap($elForm).find('.suggestion-list__item').first().should('not.be.visible'); - cy.wrap($elForm).find('.form-control').type('{uparrow}'.repeat(10)); + cy.wrap($elForm).find('.form-control').type('{uparrow}'.repeat(10), {force: true}); cy.wrap($elForm).find('.suggestion-list__item').first().should('be.visible'); } diff --git a/e2e-tests/cypress/tests/integration/channels/keyboard_shortcuts/ctrl_cmd_k_focuses_message_box_spec.js b/e2e-tests/cypress/tests/integration/channels/keyboard_shortcuts/ctrl_cmd_k_focuses_message_box_spec.js index f8a01b7b024..b71bc57b0a8 100644 --- a/e2e-tests/cypress/tests/integration/channels/keyboard_shortcuts/ctrl_cmd_k_focuses_message_box_spec.js +++ b/e2e-tests/cypress/tests/integration/channels/keyboard_shortcuts/ctrl_cmd_k_focuses_message_box_spec.js @@ -29,12 +29,14 @@ describe('Keyboard Shortcuts', () => { cy.get('body').cmdOrCtrlShortcut('K'); cy.get('#quickSwitchInput').type('T'); + // * Confirm the town-square channel is selected in the suggestion list + cy.get('#suggestionList').findByTestId('town-square').should('be.visible').and('have.class', 'suggestion--selected'); + // # Press down arrow cy.wait(TIMEOUTS.HALF_SEC); cy.get('body').type('{downarrow}'); - cy.get('body').type('{downarrow}'); - // * Confirm the offtopic channel is selected in the suggestion list + // * Confirm the off-topic channel is selected in the suggestion list cy.get('#suggestionList').findByTestId('off-topic').should('be.visible').and('have.class', 'suggestion--selected'); // # Press ENTER diff --git a/e2e-tests/cypress/tests/integration/channels/keyboard_shortcuts/dot_menu_spec.js b/e2e-tests/cypress/tests/integration/channels/keyboard_shortcuts/dot_menu_spec.js index 6e495bc4546..db50836ddf7 100644 --- a/e2e-tests/cypress/tests/integration/channels/keyboard_shortcuts/dot_menu_spec.js +++ b/e2e-tests/cypress/tests/integration/channels/keyboard_shortcuts/dot_menu_spec.js @@ -94,10 +94,8 @@ describe('Keyboard Shortcuts', () => { }); cy.getLastPostId().then((postId) => { - // * verify Save not shown in webview - cy.findByText('Save').should('not.exist'); - - cy.viewport('iphone-6'); + // * verify Saved not shown in webview + cy.findByText('Saved').should('not.exist'); // # Save Post cy.uiPostDropdownMenuShortcut(postId, 'Save', 'S', 'RHS_ROOT'); diff --git a/e2e-tests/cypress/tests/integration/channels/messaging/ctrl_cmd_k_find_gm_by_matching_name_spec.js b/e2e-tests/cypress/tests/integration/channels/messaging/ctrl_cmd_k_find_gm_by_matching_name_spec.js index 69baab83bd2..7a8a8437a94 100644 --- a/e2e-tests/cypress/tests/integration/channels/messaging/ctrl_cmd_k_find_gm_by_matching_name_spec.js +++ b/e2e-tests/cypress/tests/integration/channels/messaging/ctrl_cmd_k_find_gm_by_matching_name_spec.js @@ -42,6 +42,7 @@ describe('Messaging', () => { cy.apiCreateGroupChannel([firstUser.id, secondUser.id, thirdUser.id]).then(({channel}) => { // # Visit the newly created group message cy.visit(`/${testTeam.name}/channels/${channel.name}`); + cy.postMessage('hello'); // # Go to off-topic cy.get('#sidebarItem_off-topic').click(); diff --git a/e2e-tests/cypress/tests/integration/channels/messaging/message_with_gif_spec.js b/e2e-tests/cypress/tests/integration/channels/messaging/message_with_gif_spec.js index 9b1727949f0..3de2c7dabee 100644 --- a/e2e-tests/cypress/tests/integration/channels/messaging/message_with_gif_spec.js +++ b/e2e-tests/cypress/tests/integration/channels/messaging/message_with_gif_spec.js @@ -36,11 +36,11 @@ describe('Show GIF images properly', () => { cy.url().should('include', offtopiclink); // # Post tenor GIF - cy.postMessage('https://c.tenor.com/Ztva2YFROSkAAAAi/duck-searching.gif'); + cy.postMessage('https://media.tenor.com/yCFHzEvKa9MAAAAi/hello.gif'); cy.getLastPostId().as('postId').then((postId) => { // * Validate image size - cy.get(`#post_${postId}`).find('.attachment__image').should('have.css', 'width', '137px'); + cy.get(`#post_${postId}`).find('.attachment__image').should('have.css', 'width', '189px'); }); // # Post giphy GIF diff --git a/e2e-tests/cypress/tests/integration/channels/notifications/at_icon_still_shows_mentions_list_with_deactivated_triggers_spec.js b/e2e-tests/cypress/tests/integration/channels/notifications/at_icon_still_shows_mentions_list_with_deactivated_triggers_spec.js index 44d4065051b..c50a58c9982 100644 --- a/e2e-tests/cypress/tests/integration/channels/notifications/at_icon_still_shows_mentions_list_with_deactivated_triggers_spec.js +++ b/e2e-tests/cypress/tests/integration/channels/notifications/at_icon_still_shows_mentions_list_with_deactivated_triggers_spec.js @@ -23,12 +23,12 @@ describe('Notifications', () => { // # Open 'Settings' modal cy.uiOpenSettingsModal().within(() => { - // # Open 'Words That Trigger Mentions' setting and uncheck all the checkboxes - cy.findByRole('heading', {name: 'Words That Trigger Mentions'}).should('be.visible').click(); + // # Open 'Keywords that trigger Notifications' setting and uncheck all the checkboxes + cy.findByRole('heading', {name: 'Keywords that trigger Notifications'}).should('be.visible').click(); cy.findByRole('checkbox', {name: `Your case-sensitive first name "${otherUser.first_name}"`}).should('not.be.checked'); cy.findByRole('checkbox', {name: `Your non case-sensitive username "${otherUser.username}"`}).should('not.be.checked'); cy.findByRole('checkbox', {name: 'Channel-wide mentions "@channel", "@all", "@here"'}).click().should('not.be.checked'); - cy.findByRole('checkbox', {name: 'Other non case-sensitive words, separated by commas:'}).should('not.be.checked'); + cy.findByRole('checkbox', {name: 'Other non case-sensitive words, press Tab or use commas to separate keywords:'}).should('not.be.checked'); // # Save then close the modal cy.uiSaveAndClose(); diff --git a/e2e-tests/cypress/tests/integration/channels/notifications/at_mentions_spec.js b/e2e-tests/cypress/tests/integration/channels/notifications/at_mentions_spec.js index 822e2de01a2..716c0319a49 100644 --- a/e2e-tests/cypress/tests/integration/channels/notifications/at_mentions_spec.js +++ b/e2e-tests/cypress/tests/integration/channels/notifications/at_mentions_spec.js @@ -243,14 +243,16 @@ function setNotificationSettings(desiredSettings = {first: true, username: true, // Navigate to settings modal cy.uiOpenSettingsModal(); - // Notifications header should be visible - cy.get('#notificationSettingsTitle'). - scrollIntoView(). + // # Click on notifications tab + cy.findByRoleExtended('tab', {name: 'Notifications'}). should('be.visible'). - and('contain', 'Notifications'); + click(); + + // Notifications header should be visible + cy.findAllByText('Notifications').should('be.visible'); // Open up 'Words that trigger mentions' sub-section - cy.get('#keysTitle'). + cy.findByText('Keywords that trigger Notifications'). scrollIntoView(). click(); @@ -270,8 +272,8 @@ function setNotificationSettings(desiredSettings = {first: true, username: true, // Set Custom field if (desiredSettings.custom && desiredSettings.customText) { cy.get('#notificationTriggerCustomText'). - clear(). - type(desiredSettings.customText); + type(desiredSettings.customText, {force: true}). + tab(); } // Click “Save” and close modal diff --git a/e2e-tests/cypress/tests/integration/channels/notifications/deselect_username_mention_trigger_spec.js b/e2e-tests/cypress/tests/integration/channels/notifications/deselect_username_mention_trigger_spec.js index 25544935bb7..93bd1e23550 100644 --- a/e2e-tests/cypress/tests/integration/channels/notifications/deselect_username_mention_trigger_spec.js +++ b/e2e-tests/cypress/tests/integration/channels/notifications/deselect_username_mention_trigger_spec.js @@ -29,14 +29,16 @@ describe('Notifications', () => { // # Open 'Settings' modal cy.uiOpenSettingsModal().within(() => { - cy.get('#keysEdit').should('be.visible').click(); + // # Open 'Keywords that trigger Notifications' setting + cy.findByRole('heading', {name: 'Keywords that trigger Notifications'}).should('be.visible').click(); // * As otherUser, ensure that 'Your non-case sensitive username' is not checked - cy.get('#notificationTriggerUsername').should('not.be.checked'); + cy.findByRole('checkbox', {name: `Your non case-sensitive username "${otherUser.username}"`}).should('not.be.checked'); - // # Close the modal - cy.get('#accountSettingsHeader').find('button').should('be.visible').click(); + // # Save then close the modal + cy.uiSaveAndClose(); }); + cy.apiLogout(); // # Login as sysadmin diff --git a/e2e-tests/cypress/tests/integration/channels/plugins/marketplace/ui_spec.js b/e2e-tests/cypress/tests/integration/channels/plugins/marketplace/ui_spec.js index 9b36d0e96b1..f5cc983ba84 100644 --- a/e2e-tests/cypress/tests/integration/channels/plugins/marketplace/ui_spec.js +++ b/e2e-tests/cypress/tests/integration/channels/plugins/marketplace/ui_spec.js @@ -17,6 +17,7 @@ describe('Plugin Marketplace', () => { before(() => { cy.shouldNotRunOnCloudEdition(); + cy.shouldHaveFeatureFlag('StreamlinedMarketplace', 'false'); // https://mattermost.atlassian.net/browse/MM-54230 cy.shouldHavePluginUploadEnabled(); cy.apiInitSetup().then(({team}) => { @@ -72,6 +73,7 @@ describe('Plugin Marketplace', () => { cy.get('#marketplaceTabs-pane-allListing').should('be.visible'); }); + // https://mattermost.atlassian.net/browse/MM-54230 it('MM-T2001 autofocus on search plugin input box', () => { cy.uiClose(); @@ -82,6 +84,7 @@ describe('Plugin Marketplace', () => { cy.findByPlaceholderText('Search Marketplace').should('be.focused'); }); + // https://mattermost.atlassian.net/browse/MM-54230 it('render the list of all plugins by default', () => { // * Verify all plugins tab should be active cy.get('#marketplaceTabs-tab-allListing').should('be.visible').parent().should('have.class', 'active'); @@ -92,6 +95,7 @@ describe('Plugin Marketplace', () => { cy.get('#marketplaceTabs-pane-installed').should('not.exist'); }); + // https://mattermost.atlassian.net/browse/MM-54230 // this test uses exist, not visible, due to issues with Cypress it('render the list of installed plugins on demand', () => { // # Click on installed plugins tab @@ -117,6 +121,7 @@ describe('Plugin Marketplace', () => { cy.get('#modal_marketplace').should('not.exist'); }); + // https://mattermost.atlassian.net/browse/MM-54230 it('should filter all on search', () => { // # Load all plugins before searching cy.get('.more-modal__row').should('have.length', 15); @@ -136,6 +141,7 @@ describe('Plugin Marketplace', () => { should('have.length', 1); }); + // https://mattermost.atlassian.net/browse/MM-54230 it('should show an error bar on failing to filter', () => { // # Enable Plugin Marketplace cy.apiUpdateConfig({ @@ -168,6 +174,7 @@ describe('Plugin Marketplace', () => { cy.get('#marketplace-plugin-com\\.mattermost\\.webex').find('.btn.btn-outline', {timeout: TIMEOUTS.ONE_MIN}).scrollIntoView().should('be.visible').and('have.text', 'Configure'); }); + // https://mattermost.atlassian.net/browse/MM-54230 it('should install a plugin from search results on demand', () => { // # Uninstall any existing webex plugin cy.apiRemovePluginById('com.mattermost.webex'); @@ -226,6 +233,7 @@ describe('Plugin Marketplace', () => { cy.get('#marketplace-plugin-github').should('be.visible'); }); + // https://mattermost.atlassian.net/browse/MM-54230 it('MM-T1986 change tab to "All Plugins" when "Install Plugins" link is clicked', () => { cy.get('#marketplaceTabs').scrollIntoView().should('be.visible').within(() => { // # Switch tab to installed plugin diff --git a/e2e-tests/cypress/tests/integration/playbooks/channels/app_bar_spec.js b/e2e-tests/cypress/tests/integration/playbooks/channels/app_bar_spec.js index d24a0858cfa..cf315009021 100644 --- a/e2e-tests/cypress/tests/integration/playbooks/channels/app_bar_spec.js +++ b/e2e-tests/cypress/tests/integration/playbooks/channels/app_bar_spec.js @@ -26,7 +26,7 @@ describe('channels > App Bar', {testIsolation: true}, () => { describe('App Bar disabled', () => { it('should not show the Playbook App Bar icon', () => { - cy.apiUpdateConfig({ExperimentalSettings: {EnableAppBar: false}}); + cy.apiUpdateConfig({ExperimentalSettings: {DisableAppBar: true}}); // # Login as testUser cy.apiLogin(testUser); @@ -41,7 +41,7 @@ describe('channels > App Bar', {testIsolation: true}, () => { describe('App Bar enabled', () => { beforeEach(() => { - cy.apiUpdateConfig({ExperimentalSettings: {EnableAppBar: true}}); + cy.apiUpdateConfig({ExperimentalSettings: {DisableAppBar: false}}); // # Login as testUser cy.apiLogin(testUser); diff --git a/e2e-tests/cypress/tests/integration/playbooks/channels/channel_header_spec.js b/e2e-tests/cypress/tests/integration/playbooks/channels/channel_header_spec.js index 5176c821a35..330f51014dd 100644 --- a/e2e-tests/cypress/tests/integration/playbooks/channels/channel_header_spec.js +++ b/e2e-tests/cypress/tests/integration/playbooks/channels/channel_header_spec.js @@ -47,7 +47,7 @@ describe('channels > channel header', {testIsolation: true}, () => { describe('App Bar enabled', () => { it('webapp should hide the Playbook channel header button', () => { cy.apiAdminLogin(); - cy.apiUpdateConfig({ExperimentalSettings: {EnableAppBar: true}}); + cy.apiUpdateConfig({ExperimentalSettings: {DisableAppBar: false}}); // # Login as testUser cy.apiLogin(testUser); @@ -65,7 +65,7 @@ describe('channels > channel header', {testIsolation: true}, () => { describe('App Bar disabled', () => { beforeEach(() => { cy.apiAdminLogin(); - cy.apiUpdateConfig({ExperimentalSettings: {EnableAppBar: false}}); + cy.apiUpdateConfig({ExperimentalSettings: {DisableAppBar: true}}); // # Login as testUser cy.apiLogin(testUser); diff --git a/e2e-tests/cypress/tests/integration/playbooks/channels/rhs/checklist_spec.js b/e2e-tests/cypress/tests/integration/playbooks/channels/rhs/checklist_spec.js index 555f642c73e..b4b2611caf3 100644 --- a/e2e-tests/cypress/tests/integration/playbooks/channels/rhs/checklist_spec.js +++ b/e2e-tests/cypress/tests/integration/playbooks/channels/rhs/checklist_spec.js @@ -164,7 +164,7 @@ describe('channels > rhs > checklist', {testIsolation: true}, () => { }); // * Verify the expected error message. - cy.verifyEphemeralMessage('Failed to find slash command /invalid'); + cy.verifyEphemeralMessage('Failed to execute slash command /invalid'); }); it('successfully runs a valid slash command', () => { diff --git a/e2e-tests/cypress/tests/integration/playbooks/channels/slash_command/commands_spec.js b/e2e-tests/cypress/tests/integration/playbooks/channels/slash_command/commands_spec.js index 65ded0b0c73..29f26641bfb 100644 --- a/e2e-tests/cypress/tests/integration/playbooks/channels/slash_command/commands_spec.js +++ b/e2e-tests/cypress/tests/integration/playbooks/channels/slash_command/commands_spec.js @@ -84,7 +84,7 @@ describe('channels > slash command > owner', {testIsolation: true}, () => { cy.findByTestId('post_textbox').clear().type('/playbook check '); // * Verify suggestions number: a single run with 4 tasks + 1 title - cy.get('.slash-command').should('have.length', 5); + cy.get('.slash-command__info').should('have.length', 5); // # Clear input cy.findByTestId('post_textbox').clear(); @@ -295,7 +295,7 @@ describe('channels > slash command > owner', {testIsolation: true}, () => { cy.findByTestId('post_textbox').clear().type('/playbook check '); // * Verify suggestions number: 2 runs * 4 tasks + 1 title - cy.get('.slash-command').should('have.length', 9); + cy.get('.slash-command__info').should('have.length', 9); // # Clear input cy.findByTestId('post_textbox').clear(); @@ -337,7 +337,7 @@ describe('channels > slash command > owner', {testIsolation: true}, () => { cy.findByTestId('post_textbox').clear().type('/playbook checkadd '); // * Verify suggestions number: 2 runs * 2 checklists + 1 title - cy.get('.slash-command').should('have.length', 5); + cy.get('.slash-command__info').should('have.length', 5); // # Clear input cy.findByTestId('post_textbox').clear(); @@ -380,7 +380,7 @@ describe('channels > slash command > owner', {testIsolation: true}, () => { cy.findByTestId('post_textbox').clear().type('/playbook checkremove '); // * Verify suggestions number: 2 runs * 4 tasks + 1 title - cy.get('.slash-command').should('have.length', 9); + cy.get('.slash-command__info').should('have.length', 9); // # Clear input cy.findByTestId('post_textbox').clear(); @@ -423,7 +423,7 @@ describe('channels > slash command > owner', {testIsolation: true}, () => { cy.findByTestId('post_textbox').clear().type('/playbook owner '); // * Verify suggestions number: 2 runs + 1 title - cy.get('.slash-command').should('have.length', 3); + cy.get('.slash-command__info').should('have.length', 3); // # Clear input cy.findByTestId('post_textbox').clear(); @@ -465,7 +465,7 @@ describe('channels > slash command > owner', {testIsolation: true}, () => { cy.findByTestId('post_textbox').clear().type('/playbook finish '); // * Verify suggestions number: 2 runs + 1 title - cy.get('.slash-command').should('have.length', 3); + cy.get('.slash-command__info').should('have.length', 3); // # Clear input cy.findByTestId('post_textbox').clear(); @@ -528,7 +528,7 @@ describe('channels > slash command > owner', {testIsolation: true}, () => { cy.findByTestId('post_textbox').clear().type('/playbook update '); // * Verify suggestions number: 2 runs + 1 title - cy.get('.slash-command').should('have.length', 3); + cy.get('.slash-command__info').should('have.length', 3); // # Clear input cy.findByTestId('post_textbox').clear(); diff --git a/e2e-tests/cypress/tests/support/api/bots.d.ts b/e2e-tests/cypress/tests/support/api/bots.d.ts index 08878ac7024..7fc8da314b8 100644 --- a/e2e-tests/cypress/tests/support/api/bots.d.ts +++ b/e2e-tests/cypress/tests/support/api/bots.d.ts @@ -43,7 +43,7 @@ declare namespace Cypress { * @example * cy.apiGetBots(); */ - apiGetBots(page: number, perPage: number, includeDeleted: boolean): Chainable<{bots: Bot[]}>; + apiGetBots(page?: number, perPage?: number, includeDeleted?: boolean): Chainable<{bots: Bot[]}>; /** * Disable bot. diff --git a/e2e-tests/cypress/tests/support/api/cloud_default_config.json b/e2e-tests/cypress/tests/support/api/cloud_default_config.json index bd250f04a60..66b75cf6318 100644 --- a/e2e-tests/cypress/tests/support/api/cloud_default_config.json +++ b/e2e-tests/cypress/tests/support/api/cloud_default_config.json @@ -1,39 +1,117 @@ { "ServiceSettings": { + "SiteURL": "http://localhost:8065", + "WebsocketURL": "", + "LicenseFileLocation": "", + "ListenAddress": ":8065", + "ConnectionSecurity": "", + "TLSCertFile": "", + "TLSKeyFile": "", + "TLSMinVer": "1.2", + "TLSStrictTransport": false, + "TLSStrictTransportMaxAge": 63072000, + "TLSOverwriteCiphers": [], + "UseLetsEncrypt": false, + "LetsEncryptCertificateCacheFile": "./config/letsencrypt.cache", + "Forward80To443": false, + "TrustedProxyIPHeader": [], + "ReadTimeout": 300, + "WriteTimeout": 300, + "IdleTimeout": 300, + "MaximumLoginAttempts": 10, + "GoroutineHealthThreshold": -1, "EnableOAuthServiceProvider": false, "EnableIncomingWebhooks": true, "EnableOutgoingWebhooks": true, "EnableCommands": true, "EnablePostUsernameOverride": false, "EnablePostIconOverride": false, + "GoogleDeveloperKey": "", "EnableLinkPreviews": false, + "EnablePermalinkPreviews": true, + "RestrictLinkPreviews": "", + "EnableTesting": false, + "EnableDeveloper": false, + "DeveloperFlags": "", + "EnableClientPerformanceDebugging": false, + "EnableOpenTracing": false, + "EnableSecurityFixAlert": true, + "EnableInsecureOutgoingConnections": false, + "AllowedUntrustedInternalConnections": "localhost", "EnableMultifactorAuthentication": false, "EnforceMultifactorAuthentication": false, "EnableUserAccessTokens": false, + "AllowCorsFrom": "", + "CorsExposedHeaders": "", + "CorsAllowCredentials": false, + "CorsDebug": false, + "AllowCookiesForSubdomains": false, + "ExtendSessionLengthWithActivity": true, + "SessionLengthWebInDays": 30, + "SessionLengthWebInHours": 720, + "SessionLengthMobileInDays": 30, + "SessionLengthMobileInHours": 720, + "SessionLengthSSOInDays": 30, + "SessionLengthSSOInHours": 720, + "SessionCacheInMinutes": 10, + "SessionIdleTimeoutInMinutes": 43200, + "WebsocketSecurePort": 443, + "WebsocketPort": 80, + "WebserverMode": "gzip", + "EnableGifPicker": false, + "GiphySdkKey": "s0glxvzVg9azvPipKxcPLpXV0q1x1fVP", "EnableCustomEmoji": false, "EnableEmojiPicker": true, - "EnableGifPicker": false, - "GfycatAPIKey": "2_KtH_W5", - "GfycatAPISecret": "3wLVZPiswc3DnaiaFoLkDvB4X0IV6CpMkj4tf2inJRsBY6-FnkT08zGmppWFgeof", "PostEditTimeLimit": -1, + "TimeBetweenUserTypingUpdatesMilliseconds": 5000, + "EnablePostSearch": true, + "EnableFileSearch": true, + "MinimumHashtagLength": 3, + "EnableUserTypingMessages": true, + "EnableChannelViewedMessages": true, + "EnableUserStatuses": true, + "ExperimentalEnableAuthenticationTransfer": true, + "ClusterLogTimeoutMilliseconds": 2000, "EnablePreviewFeatures": true, "EnableTutorial": true, "EnableOnboardingFlow": false, "ExperimentalEnableDefaultChannelLeaveJoinMessages": true, "ExperimentalGroupUnreadChannels": "disabled", "EnableAPITeamDeletion": true, + "EnableAPITriggerAdminNotifications": false, + "EnableAPIUserDeletion": false, "ExperimentalEnableHardenedMode": false, + "ExperimentalStrictCSRFEnforcement": false, "EnableEmailInvitations": true, + "DisableBotsWhenOwnerIsDeactivated": true, "EnableBotAccountCreation": true, "EnableSVGs": true, "EnableLatex": false, - "EnableLegacySidebar": false, + "EnableInlineLatex": true, + "PostPriority": true, + "AllowPersistentNotifications": true, + "AllowPersistentNotificationsForGuests": false, + "PersistentNotificationIntervalMinutes": 5, + "PersistentNotificationMaxCount": 6, + "PersistentNotificationMaxRecipients": 5, + "EnableAPIChannelDeletion": false, + "EnableLocalMode": false, + "LocalModeSocketLocation": "/var/tmp/mattermost_local.socket", + "EnableAWSMetering": false, + "SplitKey": "", + "FeatureFlagSyncIntervalSeconds": 30, + "DebugSplit": false, "ThreadAutoFollow": true, - "CollapsedThreads": "disabled" + "CollapsedThreads": "disabled", + "ManagedResourcePaths": "", + "EnableCustomGroups": true, + "SelfHostedPurchase": true, + "AllowSyncedDrafts": true }, "TeamSettings": { "SiteName": "Mattermost", "MaxUsersPerTeam": 2000, + "EnableJoinLeaveMessageByDefault": true, "EnableUserCreation": true, "EnableOpenServer": true, "EnableUserDeactivation": false, @@ -43,6 +121,7 @@ "CustomBrandText": "", "CustomDescriptionText": "", "RestrictDirectMessage": "any", + "EnableLastActiveTime": true, "UserStatusAwayTimeout": 300, "MaxChannelsPerTeam": 2000, "MaxNotificationsPerChannel": 1000, @@ -54,12 +133,19 @@ "ExperimentalPrimaryTeam": "", "ExperimentalDefaultChannels": [] }, + "ClientRequirements": { + "AndroidLatestVersion": "", + "AndroidMinVersion": "", + "IosLatestVersion": "", + "IosMinVersion": "" + }, "PasswordSettings": { "MinimumLength": 5, "Lowercase": false, "Number": false, "Uppercase": false, - "Symbol": false + "Symbol": false, + "EnableForgotLink": true }, "EmailSettings": { "EnableSignUpWithEmail": true, @@ -69,14 +155,27 @@ "UseChannelInEmailNotifications": false, "RequireEmailVerification": false, "FeedbackName": "", + "FeedbackEmail": "test@example.com", + "ReplyToAddress": "test@example.com", "FeedbackOrganization": "", + "EnableSMTPAuth": false, + "SMTPUsername": "", + "SMTPPassword": "", + "SMTPServer": "localhost", + "SMTPPort": "10025", + "SMTPServerTimeout": 10, + "ConnectionSecurity": "", "SendPushNotifications": true, "PushNotificationServer": "https://push-test.mattermost.com", + "PushNotificationServerType": "custom", + "PushNotificationServerLocation": "us", "PushNotificationContents": "generic", + "PushNotificationBuffer": 1000, "EnableEmailBatching": false, "EmailBatchingBufferSize": 256, "EmailBatchingInterval": 30, "EnablePreviewModeBanner": true, + "SkipServerCertificateVerification": false, "EmailNotificationContentsType": "full", "LoginButtonColor": "#0000", "LoginButtonBorderColor": "#2389D7", @@ -87,6 +186,12 @@ "ShowFullName": true }, "SupportSettings": { + "TermsOfServiceLink": "https://mattermost.com/pl/terms-of-use/", + "PrivacyPolicyLink": "https://mattermost.com/pl/privacy-policy/", + "AboutLink": "https://mattermost.com/pl/about-mattermost", + "HelpLink": "https://mattermost.com/pl/help/", + "ReportAProblemLink": "https://mattermost.com/pl/report-a-bug", + "ForgotPasswordLink": "", "SupportEmail": "", "CustomTermsOfServiceEnabled": false, "CustomTermsOfServiceReAcceptancePeriod": 365, @@ -99,7 +204,10 @@ "BannerTextColor": "#333333", "AllowBannerDismissal": true, "AdminNoticesEnabled": false, - "UserNoticesEnabled": false + "UserNoticesEnabled": false, + "NoticesURL": "https://notices.mattermost.com/", + "NoticesFetchFrequency": 3600, + "NoticesSkipCache": false }, "ThemeSettings": { "EnableThemeSelection": true, @@ -114,7 +222,10 @@ "Scope": "", "AuthEndpoint": "", "TokenEndpoint": "", - "UserAPIEndpoint": "" + "UserAPIEndpoint": "", + "DiscoveryEndpoint": "", + "ButtonText": "", + "ButtonColor": "" }, "GoogleSettings": { "Enable": false, @@ -123,7 +234,10 @@ "Scope": "profile email", "AuthEndpoint": "https://accounts.google.com/o/oauth2/v2/auth", "TokenEndpoint": "https://www.googleapis.com/oauth2/v4/token", - "UserAPIEndpoint": "https://people.googleapis.com/v1/people/me?personFields=names,emailAddresses,nicknames,metadata" + "UserAPIEndpoint": "https://people.googleapis.com/v1/people/me?personFields=names,emailAddresses,nicknames,metadata", + "DiscoveryEndpoint": "", + "ButtonText": "", + "ButtonColor": "" }, "Office365Settings": { "Enable": false, @@ -133,8 +247,21 @@ "AuthEndpoint": "https://login.microsoftonline.com/common/oauth2/v2.0/authorize", "TokenEndpoint": "https://login.microsoftonline.com/common/oauth2/v2.0/token", "UserAPIEndpoint": "https://graph.microsoft.com/v1.0/me", + "DiscoveryEndpoint": "", "DirectoryId": "" }, + "OpenIdSettings": { + "Enable": false, + "Secret": "", + "Id": "", + "Scope": "profile openid email", + "AuthEndpoint": "", + "TokenEndpoint": "", + "UserAPIEndpoint": "", + "DiscoveryEndpoint": "", + "ButtonText": "", + "ButtonColor": "#145DBF" + }, "LdapSettings": { "Enable": true, "EnableSync": false, @@ -162,6 +289,8 @@ "PictureAttribute": "", "SyncIntervalMinutes": 10000, "SkipCertificateVerification": true, + "PublicCertificateFile": "", + "PrivateKeyFile": "", "QueryTimeout": 60, "MaxPageSize": 500, "LoginFieldName": "", @@ -172,7 +301,9 @@ }, "ComplianceSettings": { "Enable": false, - "EnableDaily": false + "Directory": "./data/", + "EnableDaily": false, + "BatchSize": 30000 }, "LocalizationSettings": { "DefaultServerLocale": "en", @@ -183,12 +314,14 @@ "Enable": false, "EnableSyncWithLdap": false, "EnableSyncWithLdapIncludeAuth": false, + "IgnoreGuestsLdapSync": false, "Verify": true, "Encrypt": true, "SignRequest": false, "IdpURL": "", "IdpDescriptorURL": "", "IdpMetadataURL": "", + "ServiceProviderIdentifier": "", "AssertionConsumerServiceURL": "", "SignatureAlgorithm": "RSAwithSHA1", "CanonicalAlgorithm": "Canonical1.0", @@ -213,18 +346,32 @@ "LoginButtonBorderColor": "#2389D7", "LoginButtonTextColor": "#ffffff" }, + "NativeAppSettings": { + "AppCustomURLSchemes": ["mmauth://", "mmauthbeta://"], + "AppDownloadLink": "https://mattermost.com/pl/download-apps", + "AndroidAppDownloadLink": "https://mattermost.com/pl/android-app/", + "IosAppDownloadLink": "https://mattermost.com/pl/ios-app/" + }, "ClusterSettings": { "Enable": false }, "ExperimentalSettings": { "RestrictSystemAdmin": true }, + "AnalyticsSettings": { + "MaxUsersForStatistics": 2500 + }, "DataRetentionSettings": { "EnableMessageDeletion": false, "EnableFileDeletion": false, + "EnableBoardsDeletion": false, "MessageRetentionDays": 365, "FileRetentionDays": 365, - "DeletionJobStartTime": "02:00" + "BoardsRetentionDays": 365, + "DeletionJobStartTime": "02:00", + "BatchSize": 3000, + "TimeBetweenBatchesMilliseconds": 100, + "RetentionIdsBatchSize": 100 }, "MessageExportSettings": { "EnableExport": false, @@ -232,37 +379,51 @@ "DailyRunTime": "01:00", "ExportFromTimestamp": 0, "BatchSize": 10000, + "DownloadExportResults": false, "GlobalRelaySettings": { "CustomerType": "A9", "SMTPUsername": "", "SMTPPassword": "", - "EmailAddress": "" + "EmailAddress": "", + "SMTPServerTimeout": 1800 } }, + "ProductSettings": {}, "PluginSettings": { "Enable": true, + "EnableUploads": true, + "AllowInsecureDownloadURL": false, + "EnableHealthCheck": true, + "Directory": "./plugins", + "ClientDirectory": "./client/plugins", "Plugins": {}, "PluginStates": { + "com.mattermost.calls": { + "Enable": false + }, "com.mattermost.nps": { "Enable": false }, - "com.mattermost.plugin-incident-response": { - "Enable": false - }, - "com.mattermost.plugin-incident-management": { - "Enable": false - }, - "focalboard": { - "Enable": false + "playbooks": { + "Enable": true } - } + }, + "EnableMarketplace": true, + "EnableRemoteMarketplace": true, + "AutomaticPrepackagedPlugins": true, + "RequirePluginSignature": false, + "MarketplaceURL": "https://api.integrations.mattermost.com", + "SignaturePublicKeyFiles": [], + "ChimeraOAuthProxyURL": "" }, "DisplaySettings": { "CustomURLSchemes": [], + "MaxMarkdownNodes": 0, "ExperimentalTimezone": false }, "GuestAccountsSettings": { "Enable": true, + "HideTags": false, "AllowEmailAccounts": true, "EnforceMultifactorAuthentication": false, "RestrictCreationToDomains": "" @@ -272,5 +433,13 @@ "ImageProxyType": "local", "RemoteImageProxyURL": "", "RemoteImageProxyOptions": "" + }, + "ImportSettings": { + "Directory": "./import", + "RetentionDays": 30 + }, + "ExportSettings": { + "Directory": "./export", + "RetentionDays": 30 } } diff --git a/e2e-tests/cypress/tests/support/api/group.ts b/e2e-tests/cypress/tests/support/api/group.ts new file mode 100644 index 00000000000..e66654e3336 --- /dev/null +++ b/e2e-tests/cypress/tests/support/api/group.ts @@ -0,0 +1,45 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +// ***************************************************************************** +// Groups +// https://api.mattermost.com/#tag/groups +// ***************************************************************************** + +import {ChainableT} from '../../types'; + +function apiCreateCustomUserGroup(displayName: string, name: string, userIds: string[]): ChainableT> { + return cy.request({ + headers: {'X-Requested-With': 'XMLHttpRequest'}, + url: '/api/v4/groups', + method: 'POST', + body: { + display_name: displayName, + name, + source: 'custom', + allow_reference: true, + user_ids: userIds, + }, + }).then((response) => { + expect(response.status).to.equal(201); + return cy.wrap(response); + }); +} + +Cypress.Commands.add('apiCreateCustomUserGroup', apiCreateCustomUserGroup); + +declare global { + // eslint-disable-next-line @typescript-eslint/no-namespace + namespace Cypress { + interface Chainable { + + /** + * Create custom user group + * @param {string} displayName - the display name of the group + * @param {string} name - the @ mentionable name of the group + * @param {string[]} userIds - users to add to the group + */ + apiCreateCustomUserGroup: typeof apiCreateCustomUserGroup; + } + } +} diff --git a/e2e-tests/cypress/tests/support/api/index.js b/e2e-tests/cypress/tests/support/api/index.js index e9edc95e1bc..6d38b1883bd 100644 --- a/e2e-tests/cypress/tests/support/api/index.js +++ b/e2e-tests/cypress/tests/support/api/index.js @@ -8,6 +8,7 @@ import './cloud'; import './cluster'; import './common'; import './data_retention'; +import './group'; import './keycloak'; import './ldap'; import './playbooks'; diff --git a/e2e-tests/cypress/tests/support/api/on_prem_default_config.json b/e2e-tests/cypress/tests/support/api/on_prem_default_config.json index 739b0bae36d..c18ce4cd702 100644 --- a/e2e-tests/cypress/tests/support/api/on_prem_default_config.json +++ b/e2e-tests/cypress/tests/support/api/on_prem_default_config.json @@ -12,22 +12,28 @@ "TLSStrictTransportMaxAge": 63072000, "TLSOverwriteCiphers": [], "UseLetsEncrypt": false, + "LetsEncryptCertificateCacheFile": "./config/letsencrypt.cache", "Forward80To443": false, "TrustedProxyIPHeader": [], "ReadTimeout": 300, "WriteTimeout": 300, "IdleTimeout": 300, + "MaximumLoginAttempts": 10, "GoroutineHealthThreshold": -1, - "GoogleDeveloperKey": "", "EnableOAuthServiceProvider": false, "EnableIncomingWebhooks": true, "EnableOutgoingWebhooks": true, "EnableCommands": true, "EnablePostUsernameOverride": false, "EnablePostIconOverride": false, + "GoogleDeveloperKey": "", "EnableLinkPreviews": false, + "EnablePermalinkPreviews": true, + "RestrictLinkPreviews": "", "EnableTesting": false, "EnableDeveloper": false, + "DeveloperFlags": "", + "EnableClientPerformanceDebugging": false, "EnableOpenTracing": false, "EnableSecurityFixAlert": true, "EnableInsecureOutgoingConnections": false, @@ -41,22 +47,25 @@ "CorsDebug": false, "AllowCookiesForSubdomains": false, "ExtendSessionLengthWithActivity": true, + "SessionLengthWebInDays": 30, "SessionLengthWebInHours": 720, + "SessionLengthMobileInDays": 30, "SessionLengthMobileInHours": 720, + "SessionLengthSSOInDays": 30, "SessionLengthSSOInHours": 720, "SessionCacheInMinutes": 10, "SessionIdleTimeoutInMinutes": 43200, "WebsocketSecurePort": 443, "WebsocketPort": 80, "WebserverMode": "gzip", + "EnableGifPicker": false, + "GiphySdkKey": "s0glxvzVg9azvPipKxcPLpXV0q1x1fVP", "EnableCustomEmoji": false, "EnableEmojiPicker": true, - "EnableGifPicker": false, - "GfycatAPIKey": "2_KtH_W5", - "GfycatAPISecret": "3wLVZPiswc3DnaiaFoLkDvB4X0IV6CpMkj4tf2inJRsBY6-FnkT08zGmppWFgeof", "PostEditTimeLimit": -1, "TimeBetweenUserTypingUpdatesMilliseconds": 5000, "EnablePostSearch": true, + "EnableFileSearch": true, "MinimumHashtagLength": 3, "EnableUserTypingMessages": true, "EnableChannelViewedMessages": true, @@ -69,6 +78,8 @@ "ExperimentalEnableDefaultChannelLeaveJoinMessages": true, "ExperimentalGroupUnreadChannels": "disabled", "EnableAPITeamDeletion": true, + "EnableAPITriggerAdminNotifications": false, + "EnableAPIUserDeletion": false, "ExperimentalEnableHardenedMode": false, "ExperimentalStrictCSRFEnforcement": false, "EnableEmailInvitations": true, @@ -76,13 +87,31 @@ "EnableBotAccountCreation": true, "EnableSVGs": true, "EnableLatex": false, - "EnableLegacySidebar": false, + "EnableInlineLatex": true, + "PostPriority": true, + "AllowPersistentNotifications": true, + "AllowPersistentNotificationsForGuests": false, + "PersistentNotificationIntervalMinutes": 5, + "PersistentNotificationMaxCount": 6, + "PersistentNotificationMaxRecipients": 5, + "EnableAPIChannelDeletion": false, + "EnableLocalMode": false, + "LocalModeSocketLocation": "/var/tmp/mattermost_local.socket", + "EnableAWSMetering": false, + "SplitKey": "", + "FeatureFlagSyncIntervalSeconds": 30, + "DebugSplit": false, "ThreadAutoFollow": true, - "CollapsedThreads": "disabled" + "CollapsedThreads": "disabled", + "ManagedResourcePaths": "", + "EnableCustomGroups": true, + "SelfHostedPurchase": true, + "AllowSyncedDrafts": true }, "TeamSettings": { "SiteName": "Mattermost", "MaxUsersPerTeam": 2000, + "EnableJoinLeaveMessageByDefault": true, "EnableUserCreation": true, "EnableOpenServer": true, "EnableUserDeactivation": false, @@ -92,6 +121,7 @@ "CustomBrandText": "", "CustomDescriptionText": "", "RestrictDirectMessage": "any", + "EnableLastActiveTime": true, "UserStatusAwayTimeout": 300, "MaxChannelsPerTeam": 2000, "MaxNotificationsPerChannel": 1000, @@ -110,26 +140,37 @@ "IosMinVersion": "" }, "SqlSettings": { + "DriverName": "postgres", + "DataSource": "postgres://mmuser:mostest@localhost/mattermost_test?sslmode=disable\u0026connect_timeout=10\u0026binary_parameters=yes", "DataSourceReplicas": [], "DataSourceSearchReplicas": [], "MaxIdleConns": 20, "ConnMaxLifetimeMilliseconds": 3600000, + "ConnMaxIdleTimeMilliseconds": 300000, "MaxOpenConns": 300, "Trace": false, "AtRestEncryptKey": "", - "QueryTimeout": 30 + "QueryTimeout": 30, + "DisableDatabaseSearch": false, + "MigrationsStatementTimeoutSeconds": 100000, + "ReplicaLagSettings": [], + "ReplicaMonitorIntervalSeconds": 5 }, "LogSettings": { "EnableConsole": true, "ConsoleLevel": "DEBUG", "ConsoleJson": true, + "EnableColor": false, "EnableFile": true, "FileLevel": "INFO", "FileJson": true, "FileLocation": "", "EnableWebhookDebugging": true, "EnableDiagnostics": true, - "EnableSentry": false + "VerboseDiagnostics": false, + "EnableSentry": false, + "AdvancedLoggingJSON": {}, + "AdvancedLoggingConfig": "" }, "ExperimentalAuditSettings": { "FileEnabled": false, @@ -138,16 +179,21 @@ "FileMaxAgeDays": 0, "FileMaxBackups": 0, "FileCompress": false, - "FileMaxQueueSize": 1000 + "FileMaxQueueSize": 1000, + "AdvancedLoggingJSON": {}, + "AdvancedLoggingConfig": "" }, "NotificationLogSettings": { "EnableConsole": true, "ConsoleLevel": "DEBUG", "ConsoleJson": true, + "EnableColor": false, "EnableFile": true, "FileLevel": "INFO", "FileJson": true, - "FileLocation": "" + "FileLocation": "", + "AdvancedLoggingJSON": {}, + "AdvancedLoggingConfig": "" }, "PasswordSettings": { "MinimumLength": 5, @@ -155,27 +201,48 @@ "Number": false, "Uppercase": false, "Symbol": false, - "Enable": false + "EnableForgotLink": true }, "FileSettings": { "EnableFileAttachments": true, "EnableMobileUpload": true, "EnableMobileDownload": true, "MaxFileSize": 104857600, + "MaxImageResolution": 33177600, + "MaxImageDecoderConcurrency": -1, "DriverName": "local", "Directory": "./data/", "EnablePublicLink": false, + "ExtractContent": true, + "ArchiveRecursion": false, "PublicLinkSalt": "", "InitialFont": "nunito-bold.ttf", "AmazonS3AccessKeyId": "", "AmazonS3SecretAccessKey": "", "AmazonS3Bucket": "", + "AmazonS3PathPrefix": "", "AmazonS3Region": "", "AmazonS3Endpoint": "s3.amazonaws.com", "AmazonS3SSL": true, "AmazonS3SignV2": false, "AmazonS3SSE": false, - "AmazonS3Trace": false + "AmazonS3Trace": false, + "AmazonS3RequestTimeoutMilliseconds": 30000, + "DedicatedExportStore": false, + "ExportDriverName": "local", + "ExportDirectory": "./data/", + "ExportAmazonS3AccessKeyId": "", + "ExportAmazonS3SecretAccessKey": "", + "ExportAmazonS3Bucket": "", + "ExportAmazonS3PathPrefix": "", + "ExportAmazonS3Region": "", + "ExportAmazonS3Endpoint": "s3.amazonaws.com", + "ExportAmazonS3SSL": true, + "ExportAmazonS3SignV2": false, + "ExportAmazonS3SSE": false, + "ExportAmazonS3Trace": false, + "ExportAmazonS3RequestTimeoutMilliseconds": 30000, + "ExportAmazonS3PresignExpiresSeconds": 21600 }, "EmailSettings": { "EnableSignUpWithEmail": true, @@ -197,7 +264,10 @@ "ConnectionSecurity": "", "SendPushNotifications": true, "PushNotificationServer": "https://push-test.mattermost.com", + "PushNotificationServerType": "custom", + "PushNotificationServerLocation": "us", "PushNotificationContents": "generic", + "PushNotificationBuffer": 1000, "EnableEmailBatching": false, "EmailBatchingBufferSize": 256, "EmailBatchingInterval": 30, @@ -224,9 +294,10 @@ "SupportSettings": { "TermsOfServiceLink": "https://mattermost.com/pl/terms-of-use/", "PrivacyPolicyLink": "https://mattermost.com/pl/privacy-policy/", - "AboutLink": "https://docs.mattermost.com/pl/about-mattermost", + "AboutLink": "https://mattermost.com/pl/about-mattermost", "HelpLink": "https://mattermost.com/pl/help/", "ReportAProblemLink": "https://mattermost.com/pl/report-a-bug", + "ForgotPasswordLink": "", "SupportEmail": "", "CustomTermsOfServiceEnabled": false, "CustomTermsOfServiceReAcceptancePeriod": 365, @@ -239,7 +310,10 @@ "BannerTextColor": "#333333", "AllowBannerDismissal": true, "AdminNoticesEnabled": false, - "UserNoticesEnabled": false + "UserNoticesEnabled": false, + "NoticesURL": "https://notices.mattermost.com/", + "NoticesFetchFrequency": 3600, + "NoticesSkipCache": false }, "ThemeSettings": { "EnableThemeSelection": true, @@ -254,7 +328,10 @@ "Scope": "", "AuthEndpoint": "", "TokenEndpoint": "", - "UserAPIEndpoint": "" + "UserAPIEndpoint": "", + "DiscoveryEndpoint": "", + "ButtonText": "", + "ButtonColor": "" }, "GoogleSettings": { "Enable": false, @@ -263,7 +340,10 @@ "Scope": "profile email", "AuthEndpoint": "https://accounts.google.com/o/oauth2/v2/auth", "TokenEndpoint": "https://www.googleapis.com/oauth2/v4/token", - "UserAPIEndpoint": "https://people.googleapis.com/v1/people/me?personFields=names,emailAddresses,nicknames,metadata" + "UserAPIEndpoint": "https://people.googleapis.com/v1/people/me?personFields=names,emailAddresses,nicknames,metadata", + "DiscoveryEndpoint": "", + "ButtonText": "", + "ButtonColor": "" }, "Office365Settings": { "Enable": false, @@ -273,8 +353,21 @@ "AuthEndpoint": "https://login.microsoftonline.com/common/oauth2/v2.0/authorize", "TokenEndpoint": "https://login.microsoftonline.com/common/oauth2/v2.0/token", "UserAPIEndpoint": "https://graph.microsoft.com/v1.0/me", + "DiscoveryEndpoint": "", "DirectoryId": "" }, + "OpenIdSettings": { + "Enable": false, + "Secret": "", + "Id": "", + "Scope": "profile openid email", + "AuthEndpoint": "", + "TokenEndpoint": "", + "UserAPIEndpoint": "", + "DiscoveryEndpoint": "", + "ButtonText": "", + "ButtonColor": "#145DBF" + }, "LdapSettings": { "Enable": true, "EnableSync": false, @@ -302,6 +395,8 @@ "PictureAttribute": "", "SyncIntervalMinutes": 10000, "SkipCertificateVerification": true, + "PublicCertificateFile": "", + "PrivateKeyFile": "", "QueryTimeout": 60, "MaxPageSize": 500, "LoginFieldName": "", @@ -313,7 +408,8 @@ "ComplianceSettings": { "Enable": false, "Directory": "./data/", - "EnableDaily": false + "EnableDaily": false, + "BatchSize": 30000 }, "LocalizationSettings": { "DefaultServerLocale": "en", @@ -324,12 +420,14 @@ "Enable": false, "EnableSyncWithLdap": false, "EnableSyncWithLdapIncludeAuth": false, + "IgnoreGuestsLdapSync": false, "Verify": true, "Encrypt": true, "SignRequest": false, "IdpURL": "", "IdpDescriptorURL": "", "IdpMetadataURL": "", + "ServiceProviderIdentifier": "", "AssertionConsumerServiceURL": "", "SignatureAlgorithm": "RSAwithSHA1", "CanonicalAlgorithm": "Canonical1.0", @@ -355,10 +453,28 @@ "LoginButtonTextColor": "#ffffff" }, "NativeAppSettings": { + "AppCustomURLSchemes": ["mmauth://", "mmauthbeta://"], "AppDownloadLink": "https://mattermost.com/pl/download-apps", "AndroidAppDownloadLink": "https://mattermost.com/pl/android-app/", "IosAppDownloadLink": "https://mattermost.com/pl/ios-app/" }, + "ClusterSettings": { + "Enable": false, + "ClusterName": "", + "OverrideHostname": "", + "NetworkInterface": "", + "BindAddress": "", + "AdvertiseAddress": "", + "UseIPAddress": true, + "EnableGossipCompression": true, + "EnableExperimentalGossipEncryption": false, + "ReadOnlyConfig": true, + "GossipPort": 8074, + "StreamingPort": 8075, + "MaxIdleConns": 100, + "MaxIdleConnsPerHost": 128, + "IdleConnTimeoutMilliseconds": 90000 + }, "MetricsSettings": { "Enable": false, "BlockProfileRate": 0, @@ -370,7 +486,11 @@ "LinkMetadataTimeoutMilliseconds": 5000, "RestrictSystemAdmin": false, "UseNewSAMLLibrary": false, - "DisableAppBar": false + "EnableSharedChannels": false, + "EnableRemoteClusterService": false, + "DisableAppBar": false, + "DisableRefetchingOnBrowserFocus": false, + "DelayChannelAutocomplete": false }, "AnalyticsSettings": { "MaxUsersForStatistics": 2500 @@ -393,17 +513,33 @@ "PostsAggregatorJobStartTime": "03:00", "IndexPrefix": "", "LiveIndexingBatchSize": 1, - "BulkIndexingTimeWindowSeconds": 3600, + "BatchSize": 10000, "RequestTimeoutSeconds": 30, "SkipTLSVerification": false, - "Trace": "" + "CA": "", + "ClientCert": "", + "ClientKey": "", + "Trace": "", + "IgnoredPurgeIndexes": "" + }, + "BleveSettings": { + "IndexDir": "", + "EnableIndexing": false, + "EnableSearching": false, + "EnableAutocomplete": false, + "BatchSize": 10000 }, "DataRetentionSettings": { "EnableMessageDeletion": false, "EnableFileDeletion": false, + "EnableBoardsDeletion": false, "MessageRetentionDays": 365, "FileRetentionDays": 365, - "DeletionJobStartTime": "02:00" + "BoardsRetentionDays": 365, + "DeletionJobStartTime": "02:00", + "BatchSize": 3000, + "TimeBetweenBatchesMilliseconds": 100, + "RetentionIdsBatchSize": 100 }, "MessageExportSettings": { "EnableExport": false, @@ -411,17 +547,22 @@ "DailyRunTime": "01:00", "ExportFromTimestamp": 0, "BatchSize": 10000, + "DownloadExportResults": false, "GlobalRelaySettings": { "CustomerType": "A9", "SMTPUsername": "", "SMTPPassword": "", - "EmailAddress": "" + "EmailAddress": "", + "SMTPServerTimeout": 1800 } }, "JobSettings": { "RunJobs": true, - "RunScheduler": true + "RunScheduler": true, + "CleanupJobsThresholdDays": -1, + "CleanupConfigThresholdDays": -1 }, + "ProductSettings": {}, "PluginSettings": { "Enable": true, "EnableUploads": true, @@ -431,17 +572,14 @@ "ClientDirectory": "./client/plugins", "Plugins": {}, "PluginStates": { + "com.mattermost.calls": { + "Enable": false + }, "com.mattermost.nps": { "Enable": false }, - "com.mattermost.plugin-incident-response": { - "Enable": false - }, - "com.mattermost.plugin-incident-management": { - "Enable": false - }, - "focalboard": { - "Enable": false + "playbooks": { + "Enable": true } }, "EnableMarketplace": true, @@ -449,14 +587,17 @@ "AutomaticPrepackagedPlugins": true, "RequirePluginSignature": false, "MarketplaceURL": "https://api.integrations.mattermost.com", - "SignaturePublicKeyFiles": [] + "SignaturePublicKeyFiles": [], + "ChimeraOAuthProxyURL": "" }, "DisplaySettings": { "CustomURLSchemes": [], + "MaxMarkdownNodes": 0, "ExperimentalTimezone": false }, "GuestAccountsSettings": { "Enable": true, + "HideTags": false, "AllowEmailAccounts": true, "EnforceMultifactorAuthentication": false, "RestrictCreationToDomains": "" @@ -466,5 +607,13 @@ "ImageProxyType": "local", "RemoteImageProxyURL": "", "RemoteImageProxyOptions": "" + }, + "ImportSettings": { + "Directory": "./import", + "RetentionDays": 30 + }, + "ExportSettings": { + "Directory": "./export", + "RetentionDays": 30 } } diff --git a/e2e-tests/cypress/tests/support/client-impl.js b/e2e-tests/cypress/tests/support/client-impl.js index 5c4212a82ad..248f934cb7f 100644 --- a/e2e-tests/cypress/tests/support/client-impl.js +++ b/e2e-tests/cypress/tests/support/client-impl.js @@ -1,7 +1,7 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import Client4 from 'mattermost-redux/client/client4'; +import {Client4} from '@mattermost/client'; import clientRequest from '../plugins/client_request'; diff --git a/e2e-tests/cypress/tests/support/index.d.ts b/e2e-tests/cypress/tests/support/index.d.ts index bdc9058257e..eca9acd5707 100644 --- a/e2e-tests/cypress/tests/support/index.d.ts +++ b/e2e-tests/cypress/tests/support/index.d.ts @@ -33,6 +33,7 @@ declare namespace Cypress { type UserCustomStatus = import('@mattermost/types/users').UserCustomStatus; type UserAccessToken = import('@mattermost/types/users').UserAccessToken; type DeepPartial = import('@mattermost/types/utilities').DeepPartial; + type Group = import('@mattermost/types/groups').Group; interface Chainable { tab: (options?: {shift?: boolean}) => Chainable; } diff --git a/e2e-tests/cypress/tests/support/ui/playbooks.js b/e2e-tests/cypress/tests/support/ui/playbooks.js index 23d987841de..d9d9c44ce11 100644 --- a/e2e-tests/cypress/tests/support/ui/playbooks.js +++ b/e2e-tests/cypress/tests/support/ui/playbooks.js @@ -99,7 +99,7 @@ Cypress.Commands.add('createPlaybook', (teamName, playbookName) => { // Select the playbook from the dropdown menu Cypress.Commands.add('selectPlaybookFromDropdown', (playbookName) => { cy.findByTestId('autoCompleteSelector').should('exist').within(() => { - cy.get('input').click().type(playbookName.toLowerCase()); + cy.get('input').click().type(playbookName.toLowerCase(), {force: true}); cy.get('#suggestionList').contains(playbookName).click({force: true}); }); }); diff --git a/e2e-tests/playwright/README.md b/e2e-tests/playwright/README.md index b32c4319403..867af490e16 100644 --- a/e2e-tests/playwright/README.md +++ b/e2e-tests/playwright/README.md @@ -6,7 +6,7 @@ # Typically run the local server with: cd server && make run -# Or build and distribute webapp including channels, boards and playbooks +# Or build and distribute webapp including channels and playbooks # so that their product URLs do not rely on Webpack dev server. # Especially important when running test inside the Playwright's docker container. cd webapp && make dist @@ -45,7 +45,7 @@ npm run test Change to root directory, run docker container ``` -docker run -it --rm -v "$(pwd):/mattermost/" --ipc=host mcr.microsoft.com/playwright:v1.36.0-focal /bin/bash +docker run -it --rm -v "$(pwd):/mattermost/" --ipc=host mcr.microsoft.com/playwright:v1.38.0-jammy /bin/bash ``` #### 2. Inside the docker container diff --git a/e2e-tests/playwright/global_setup.ts b/e2e-tests/playwright/global_setup.ts index 58781d9a27f..f473a15cb64 100644 --- a/e2e-tests/playwright/global_setup.ts +++ b/e2e-tests/playwright/global_setup.ts @@ -48,7 +48,7 @@ async function sysadminSetup(client: Client, user: UserProfile | null) { await client.createTeam(createRandomTeam(defaultTeam.name, defaultTeam.displayName, 'O', false)); } else if (myDefaultTeam && testConfig.resetBeforeTest) { await Promise.all( - myTeams.filter((team) => team.name !== defaultTeam.name).map((team) => client.deleteTeam(team.id)) + myTeams.filter((team) => team.name !== defaultTeam.name).map((team) => client.deleteTeam(team.id)), ); const myChannels = await client.getMyChannels(myDefaultTeam.id); @@ -61,7 +61,7 @@ async function sysadminSetup(client: Client, user: UserProfile | null) { channel.name !== 'off-topic' ); }) - .map((channel) => client.deleteChannel(channel.id)) + .map((channel) => client.deleteChannel(channel.id)), ); } @@ -170,7 +170,7 @@ async function ensureServerDeployment(client: Client) { sameClusterName, sameClusterName ? '' - : `Should have cluster name set and as expected. Got "${ClusterName}" but expected "${haClusterName}"` + : `Should have cluster name set and as expected. Got "${ClusterName}" but expected "${haClusterName}"`, ).toBe(true); const clusterInfo = await client.getClusterStatus(); @@ -179,12 +179,12 @@ async function ensureServerDeployment(client: Client) { sameCount, sameCount ? '' - : `Should match number of nodes in a cluster as expected. Got "${clusterInfo?.length}" but expected "${haClusterNodeCount}"` + : `Should match number of nodes in a cluster as expected. Got "${clusterInfo?.length}" but expected "${haClusterNodeCount}"`, ).toBe(true); clusterInfo.forEach((info) => // eslint-disable-next-line no-console - console.log(`hostname: ${info.hostname}, version: ${info.version}, config_hash: ${info.config_hash}`) + console.log(`hostname: ${info.hostname}, version: ${info.version}, config_hash: ${info.config_hash}`), ); } } diff --git a/e2e-tests/playwright/package-lock.json b/e2e-tests/playwright/package-lock.json index 7d2b5a3e8f9..10d7ea1c104 100644 --- a/e2e-tests/playwright/package-lock.json +++ b/e2e-tests/playwright/package-lock.json @@ -5,28 +5,29 @@ "requires": true, "packages": { "": { + "hasInstallScript": true, "dependencies": { "@axe-core/playwright": "4.7.3", - "@percy/cli": "1.26.2", + "@percy/cli": "1.27.1", "@percy/playwright": "1.0.4", - "@playwright/test": "1.36.1", + "@playwright/test": "1.38.0", "async-wait-until": "2.0.12", - "axe-core": "4.7.2", + "axe-core": "4.8.1", "chalk": "4.1.2", "deepmerge": "4.3.1", "dotenv": "16.3.1", "form-data": "4.0.0", "isomorphic-unfetch": "4.0.2", - "uuid": "9.0.0" + "uuid": "9.0.1" }, "devDependencies": { - "@types/uuid": "9.0.2", - "@typescript-eslint/eslint-plugin": "6.1.0", - "@typescript-eslint/parser": "6.1.0", + "@types/uuid": "9.0.4", + "@typescript-eslint/eslint-plugin": "6.7.0", + "@typescript-eslint/parser": "6.7.0", "cross-env": "7.0.3", - "eslint": "8.45.0", - "prettier": "2.8.7", - "typescript": "5.0.4" + "eslint": "8.49.0", + "prettier": "3.0.3", + "typescript": "5.2.2" } }, "node_modules/@aashutoshrathi/word-wrap": { @@ -50,31 +51,53 @@ } }, "node_modules/@babel/code-frame": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.5.tgz", - "integrity": "sha512-Xmwn266vad+6DAqEB2A6V/CcZVp62BbwVmcOJc2RPuwih1kw02TjQvWVWlcKGbBPd+8/0V5DEkOcizRGYsspYQ==", + "version": "7.22.13", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz", + "integrity": "sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==", "dependencies": { - "@babel/highlight": "^7.22.5" + "@babel/highlight": "^7.22.13", + "chalk": "^2.4.2" }, "engines": { "node": ">=6.9.0" } }, + "node_modules/@babel/code-frame/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/code-frame/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "engines": { + "node": ">=0.8.0" + } + }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.5.tgz", - "integrity": "sha512-aJXu+6lErq8ltp+JhkJUfk1MTGyuA4v7f3pA+BJ5HLfNC6nAQ0Cpi9uOquUj8Hehg0aUiHzWQbOVJGao6ztBAQ==", + "version": "7.22.19", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.19.tgz", + "integrity": "sha512-Tinq7ybnEPFFXhlYOYFiSjespWQk0dq2dRNAiMdRTOYQzEGqnnNyrTxPYHP5r6wGjlF1rFgABdDV0g8EwD6Qbg==", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/highlight": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.5.tgz", - "integrity": "sha512-BSKlD1hgnedS5XRnGOljZawtag7H1yPfQp0tdNJCHoH6AZ+Pcm9VvkrK59/Yy593Ypg0zMxH2BxD1VPYUQ7UIw==", + "version": "7.22.13", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.13.tgz", + "integrity": "sha512-C/BaXcnnvBCmHTpz/VGZ8jgtE2aYlW4hxDhseJAWZb7gqGM/qtCK6iZUb0TyKFf7BOUsBH7Q7fkRsDRhg1XklQ==", "dependencies": { "@babel/helper-validator-identifier": "^7.22.5", - "chalk": "^2.0.0", + "chalk": "^2.4.2", "js-tokens": "^4.0.0" }, "engines": { @@ -118,18 +141,18 @@ } }, "node_modules/@eslint-community/regexpp": { - "version": "4.5.1", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.5.1.tgz", - "integrity": "sha512-Z5ba73P98O1KUYCCJTUeVpja9RcGoMdncZ6T49FCUl2lN38JtCJ+3WgIDBv0AuY4WChU5PmtJmOCTlN6FZTFKQ==", + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.8.1.tgz", + "integrity": "sha512-PWiOzLIUAjN/w5K17PoF4n6sKBw0gqLHPhywmYHP4t1VFQQVYeb1yWsJwnMVEMl3tUHME7X/SJPZLmtG7XBDxQ==", "dev": true, "engines": { "node": "^12.0.0 || ^14.0.0 || >=16.0.0" } }, "node_modules/@eslint/eslintrc": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.0.tgz", - "integrity": "sha512-Lj7DECXqIVCqnqjjHMPna4vn6GJcMgul/wuS0je9OZ9gsL0zzDpKPVtcG1HaDVc+9y+qgXneTeUMbCqXJNpH1A==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.2.tgz", + "integrity": "sha512-+wvgpDsrB1YqAMdEUCcnTlpfVBH7Vqn6A/NT3D8WVXFIaKMlErPIZT3oCIAVCOtarRpMtelZLqJeU3t7WY6X6g==", "dev": true, "dependencies": { "ajv": "^6.12.4", @@ -172,18 +195,18 @@ "dev": true }, "node_modules/@eslint/js": { - "version": "8.44.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.44.0.tgz", - "integrity": "sha512-Ag+9YM4ocKQx9AarydN0KY2j0ErMHNIocPDrVo8zAE44xLTjEtz81OdR68/cydGtk6m6jDb5Za3r2useMzYmSw==", + "version": "8.49.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.49.0.tgz", + "integrity": "sha512-1S8uAY/MTJqVx0SC4epBq+N2yhuwtNwLbJYNZyhL2pO1ZVKn5HFXav5T41Ryzy9K9V7ZId2JB2oy/W4aCd9/2w==", "dev": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, "node_modules/@humanwhocodes/config-array": { - "version": "0.11.10", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.10.tgz", - "integrity": "sha512-KVVjQmNUepDVGXNuoRRdmmEjruj0KfiGSbS8LVc12LMsWDQzRXJ0qdhN8L8uUigKpfEHRhlaQFY0ib1tnUbNeQ==", + "version": "0.11.11", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.11.tgz", + "integrity": "sha512-N2brEuAadi0CcdeMXUkhbZB84eskAc8MEX1By6qEchoVywSgXPIjou4rYsl0V3Hj0ZnuGycGCjdNgockbzeWNA==", "dev": true, "dependencies": { "@humanwhocodes/object-schema": "^1.2.1", @@ -246,19 +269,19 @@ } }, "node_modules/@percy/cli": { - "version": "1.26.2", - "resolved": "https://registry.npmjs.org/@percy/cli/-/cli-1.26.2.tgz", - "integrity": "sha512-RKuylqXNfVAO+3XpUpFh67vnjB/tvSh98aDuEf1z8CNuTBSYA7YJbB5lxOnEcQ3DRMaQuemZ1zX0UyiHxLnjEw==", + "version": "1.27.1", + "resolved": "https://registry.npmjs.org/@percy/cli/-/cli-1.27.1.tgz", + "integrity": "sha512-2rU4NFe8TshV2sF+fs8bseGNohpLuRILW4t/Is7PCFYAitla1nvO0mndy0INct5VWc2KKewnq+2ZQONx3iNXLw==", "dependencies": { - "@percy/cli-app": "1.26.2", - "@percy/cli-build": "1.26.2", - "@percy/cli-command": "1.26.2", - "@percy/cli-config": "1.26.2", - "@percy/cli-exec": "1.26.2", - "@percy/cli-snapshot": "1.26.2", - "@percy/cli-upload": "1.26.2", - "@percy/client": "1.26.2", - "@percy/logger": "1.26.2" + "@percy/cli-app": "1.27.1", + "@percy/cli-build": "1.27.1", + "@percy/cli-command": "1.27.1", + "@percy/cli-config": "1.27.1", + "@percy/cli-exec": "1.27.1", + "@percy/cli-snapshot": "1.27.1", + "@percy/cli-upload": "1.27.1", + "@percy/client": "1.27.1", + "@percy/logger": "1.27.1" }, "bin": { "percy": "bin/run.cjs" @@ -268,36 +291,36 @@ } }, "node_modules/@percy/cli-app": { - "version": "1.26.2", - "resolved": "https://registry.npmjs.org/@percy/cli-app/-/cli-app-1.26.2.tgz", - "integrity": "sha512-eOg1vs6J3eINREf4SWt7YO9KJwGru2JPOXlV23fZgLaSw1iEaZqpa1NmeMlSX2NsU+J8JdAox1hkmO884QHE8A==", + "version": "1.27.1", + "resolved": "https://registry.npmjs.org/@percy/cli-app/-/cli-app-1.27.1.tgz", + "integrity": "sha512-Pat2BogPUmyciURUWbI1PQtUdPlhUrnS6aEVO1gNrulDeYF6j4LgD4SycCTj5L/xJijMXa3qFkZwW1VSdx97Yg==", "dependencies": { - "@percy/cli-command": "1.26.2", - "@percy/cli-exec": "1.26.2" + "@percy/cli-command": "1.27.1", + "@percy/cli-exec": "1.27.1" }, "engines": { "node": ">=14" } }, "node_modules/@percy/cli-build": { - "version": "1.26.2", - "resolved": "https://registry.npmjs.org/@percy/cli-build/-/cli-build-1.26.2.tgz", - "integrity": "sha512-VJJcLfWmKhVy7WHojpPtslRA8rm77umh/f7kr3kcYE2HVXDVRcH8r72zClglhYdLcrIfb/3BUCX6JXxJfgM7tg==", + "version": "1.27.1", + "resolved": "https://registry.npmjs.org/@percy/cli-build/-/cli-build-1.27.1.tgz", + "integrity": "sha512-EtktlYj4kzsBkrBSOVLKrvZWew/nq9mw/Z2m8mU4Jf7Pc779rQGUVCuI8PBhOI59c3JTjnY8KVaaj4Y+z9X+fg==", "dependencies": { - "@percy/cli-command": "1.26.2" + "@percy/cli-command": "1.27.1" }, "engines": { "node": ">=14" } }, "node_modules/@percy/cli-command": { - "version": "1.26.2", - "resolved": "https://registry.npmjs.org/@percy/cli-command/-/cli-command-1.26.2.tgz", - "integrity": "sha512-G5qALrrprUO5Zc3TrEPzqys6+clWtXjHGWVcHYMaBY0i7xwH4D0FaqtzkGRjiUxbsb353qV35B48oKsm0FJDmg==", + "version": "1.27.1", + "resolved": "https://registry.npmjs.org/@percy/cli-command/-/cli-command-1.27.1.tgz", + "integrity": "sha512-LK9YwE7qr67EtShVVjc20aJaRF8BFjZ8VWneLVoN73IYgpwBq96J4AUomoqfRpPNkpKvv6wDZFpr30xFokbrHA==", "dependencies": { - "@percy/config": "1.26.2", - "@percy/core": "1.26.2", - "@percy/logger": "1.26.2" + "@percy/config": "1.27.1", + "@percy/core": "1.27.1", + "@percy/logger": "1.27.1" }, "bin": { "percy-cli-readme": "bin/readme.js" @@ -307,22 +330,22 @@ } }, "node_modules/@percy/cli-config": { - "version": "1.26.2", - "resolved": "https://registry.npmjs.org/@percy/cli-config/-/cli-config-1.26.2.tgz", - "integrity": "sha512-E/zb2Eg1HK8JDzloY0FdguZS0nE47QwZoW9uxMw8QD2wykxIkbQeJq8Jl5a48BJ3rTj8TnKMzyTo7yMALqnISw==", + "version": "1.27.1", + "resolved": "https://registry.npmjs.org/@percy/cli-config/-/cli-config-1.27.1.tgz", + "integrity": "sha512-bEzXfdP+RRxHekZ5pULsuuKHL0K9E81IFcEqZ8DcE3gnrpWQnIFPP2fTHF6WgWuXYHzJnsoIIm90LaWYc4+BJQ==", "dependencies": { - "@percy/cli-command": "1.26.2" + "@percy/cli-command": "1.27.1" }, "engines": { "node": ">=14" } }, "node_modules/@percy/cli-exec": { - "version": "1.26.2", - "resolved": "https://registry.npmjs.org/@percy/cli-exec/-/cli-exec-1.26.2.tgz", - "integrity": "sha512-7gMzZshrtPbxHLkiduA7OcCqbhxwAZ17x3C8XdFYwzvW3MpghKhSpwK24hGH3169nvnoRjf5GzG7CHIBVaEndQ==", + "version": "1.27.1", + "resolved": "https://registry.npmjs.org/@percy/cli-exec/-/cli-exec-1.27.1.tgz", + "integrity": "sha512-8xB7Iq3fh+QfuViUeZQXZeJNoFzDf0IhYUhhlEDXPcL2sgjrLiy9QHBdq6Yn3/8gHlDJyzpkej6hzFqERYZs1g==", "dependencies": { - "@percy/cli-command": "1.26.2", + "@percy/cli-command": "1.27.1", "cross-spawn": "^7.0.3", "which": "^2.0.2" }, @@ -331,11 +354,11 @@ } }, "node_modules/@percy/cli-snapshot": { - "version": "1.26.2", - "resolved": "https://registry.npmjs.org/@percy/cli-snapshot/-/cli-snapshot-1.26.2.tgz", - "integrity": "sha512-R0rtuRkxzOfjr6N2XQyPBmL+zuXvbmNqjuBMDo8bmPy+j+b16TWFn25Q6r3jKLPdRkXVXrAMFsEsp9um1tLVrQ==", + "version": "1.27.1", + "resolved": "https://registry.npmjs.org/@percy/cli-snapshot/-/cli-snapshot-1.27.1.tgz", + "integrity": "sha512-hVYATkHSN6Mz67LuP5QqRvdblg+kLb9dqGlaMJZI0CtuQYRtkbrlPz5yII9SJGMyUYMQHbZKsc68eT7+LZHKbA==", "dependencies": { - "@percy/cli-command": "1.26.2", + "@percy/cli-command": "1.27.1", "yaml": "^2.0.0" }, "engines": { @@ -343,11 +366,11 @@ } }, "node_modules/@percy/cli-upload": { - "version": "1.26.2", - "resolved": "https://registry.npmjs.org/@percy/cli-upload/-/cli-upload-1.26.2.tgz", - "integrity": "sha512-10dJn3paz1jFQaNf59j0Kt6pkz1BDjlSstfS5f74Lwi8g3tFXLv3M/B25bOdDxLrToX1Dg8ke71wyIGTCqeLjg==", + "version": "1.27.1", + "resolved": "https://registry.npmjs.org/@percy/cli-upload/-/cli-upload-1.27.1.tgz", + "integrity": "sha512-4Kcft6ceuWy+Q5T4PKJXI63/QxCRFtCJUoU0QYrWo6TKKsescdE7/zWy6YESqHIm+XzBhZFYHwdYRsIbnjqUqQ==", "dependencies": { - "@percy/cli-command": "1.26.2", + "@percy/cli-command": "1.27.1", "fast-glob": "^3.2.11", "image-size": "^1.0.0" }, @@ -356,23 +379,23 @@ } }, "node_modules/@percy/client": { - "version": "1.26.2", - "resolved": "https://registry.npmjs.org/@percy/client/-/client-1.26.2.tgz", - "integrity": "sha512-0aJtwiL8Pt06O+4nHGBoXU2uaJmYR+Df846HCpJmKhMuS5X/OKZgd2DiOlRs61JPwP+FuHFZleUvrhngyyJrzQ==", + "version": "1.27.1", + "resolved": "https://registry.npmjs.org/@percy/client/-/client-1.27.1.tgz", + "integrity": "sha512-GvjrGUaVdjMwx8ODDja3Kdb6tXYxRxv4PLXfXg0Wbn5jIfcjicojOc0mSGxcLvRGu5tmKrE3fusMhYYtKlLihg==", "dependencies": { - "@percy/env": "1.26.2", - "@percy/logger": "1.26.2" + "@percy/env": "1.27.1", + "@percy/logger": "1.27.1" }, "engines": { "node": ">=14" } }, "node_modules/@percy/config": { - "version": "1.26.2", - "resolved": "https://registry.npmjs.org/@percy/config/-/config-1.26.2.tgz", - "integrity": "sha512-x0Y0I0m6Kcluzt/CPQzNITKYGNboWBqTa879tSxTIayk3sR7omjjQdPgNpVpwDwcx05h2sR1eFz6Plq+EF55cA==", + "version": "1.27.1", + "resolved": "https://registry.npmjs.org/@percy/config/-/config-1.27.1.tgz", + "integrity": "sha512-O7GgSrpjPeRmUmPWpFrg7368lWTJj8BtKY18Ztpq2j7bef5HCFi0AxvuOjwIH/GEwzGm/cI7ZK2kA/XGTOtpsg==", "dependencies": { - "@percy/logger": "1.26.2", + "@percy/logger": "1.27.1", "ajv": "^8.6.2", "cosmiconfig": "^8.0.0", "yaml": "^2.0.0" @@ -382,15 +405,16 @@ } }, "node_modules/@percy/core": { - "version": "1.26.2", - "resolved": "https://registry.npmjs.org/@percy/core/-/core-1.26.2.tgz", - "integrity": "sha512-cULZaGOH0xSgA5asRpuGUzcKBVItLLFJ2jhZn60n+8ueKqXpbSe7ggNKzaMoSVvNh6T5Kd8ZMYAzdg+Ijf6qeg==", + "version": "1.27.1", + "resolved": "https://registry.npmjs.org/@percy/core/-/core-1.27.1.tgz", + "integrity": "sha512-/XsOCk/XHYf9MAqcituMGrYeITiav9BW1oGM0LfmU1Gne/YnEvur4JwpJ4uJ0OrNennwIqMuAVM9Y5y/xfZ39w==", "hasInstallScript": true, "dependencies": { - "@percy/client": "1.26.2", - "@percy/config": "1.26.2", - "@percy/dom": "1.26.2", - "@percy/logger": "1.26.2", + "@percy/client": "1.27.1", + "@percy/config": "1.27.1", + "@percy/dom": "1.27.1", + "@percy/logger": "1.27.1", + "@percy/webdriver-utils": "1.27.1", "content-disposition": "^0.5.4", "cross-spawn": "^7.0.3", "extract-zip": "^2.0.1", @@ -406,25 +430,25 @@ } }, "node_modules/@percy/dom": { - "version": "1.26.2", - "resolved": "https://registry.npmjs.org/@percy/dom/-/dom-1.26.2.tgz", - "integrity": "sha512-LaB5yNk3Ds2ouOXI5YtszJY8f+Uj61fyOknOE9zWF57rSfOck92uK0DcgVtBMowQTzTIzdHn+i4ZnIBQ+4ex8g==" + "version": "1.27.1", + "resolved": "https://registry.npmjs.org/@percy/dom/-/dom-1.27.1.tgz", + "integrity": "sha512-duYA3ATjADtn05VuGIfezgdCq+8ASwcY6Mzk857DzFIlJd/6T2P8v1dy66RRp78efzmIMtRroGy9SqWonlaAEA==" }, "node_modules/@percy/env": { - "version": "1.26.2", - "resolved": "https://registry.npmjs.org/@percy/env/-/env-1.26.2.tgz", - "integrity": "sha512-aKSN1r3G/evQTc5aAEheSU5oq2sJVWcZvIqiJTKfmw2K+91R6VLEUWMemMPHSAAyi2GCrV2D2JEpFtFWEeXQyQ==", + "version": "1.27.1", + "resolved": "https://registry.npmjs.org/@percy/env/-/env-1.27.1.tgz", + "integrity": "sha512-Og0vQfV9zJftYYOY3PVtu+r7Ut/xr72BP3jH3rkeQJHnFKLkwemGbJpgSpMz7IbzWXSxYONkNfUtLvXwEsRpAw==", "dependencies": { - "@percy/logger": "1.26.2" + "@percy/logger": "1.27.1" }, "engines": { "node": ">=14" } }, "node_modules/@percy/logger": { - "version": "1.26.2", - "resolved": "https://registry.npmjs.org/@percy/logger/-/logger-1.26.2.tgz", - "integrity": "sha512-EVwPoUJWm2ESn3Llorc6D3mqwQeALgaDerlsXA7JvKOk2Xaf68CRk5akI8NBN6rZoWIx8t5WjjJksGzPvMYYnw==", + "version": "1.27.1", + "resolved": "https://registry.npmjs.org/@percy/logger/-/logger-1.27.1.tgz", + "integrity": "sha512-jH++paKzIY94fQYQBEedVTfO36I8WBqm2RNl/xzsnLuAwNEX5+JUsaPcbLrwRzhn/tCv+8h9xEBuE9nj23krIA==", "engines": { "node": ">=14" } @@ -444,29 +468,37 @@ } }, "node_modules/@percy/sdk-utils": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@percy/sdk-utils/-/sdk-utils-1.6.0.tgz", - "integrity": "sha512-4cSkWqpu7uK9zzeVwtMWrgGbP34GUlvZsWdEEt98ep6ZECQA+iGB75pOpIVwcsHKXtkDRE6fgugtxNXs5uHpMg==", + "version": "1.27.1", + "resolved": "https://registry.npmjs.org/@percy/sdk-utils/-/sdk-utils-1.27.1.tgz", + "integrity": "sha512-I12rBQYySyt8VILgYnI19obYtkaIPuFR07HVBrHLsRHiLUf92XzAtTI482qrnYwybCgU9mb9o3Kb7KtMJ0nalA==", + "engines": { + "node": ">=14" + } + }, + "node_modules/@percy/webdriver-utils": { + "version": "1.27.1", + "resolved": "https://registry.npmjs.org/@percy/webdriver-utils/-/webdriver-utils-1.27.1.tgz", + "integrity": "sha512-bPj16YmvV84egtMHyYEhlGYjwJTk0f6vopcbCYWgMgvQp78Y/RKwRm8ZpWZfW2rTqElNRudrhKdqcEeaG6qd9g==", + "dependencies": { + "@percy/config": "1.27.1", + "@percy/sdk-utils": "1.27.1" + }, "engines": { "node": ">=14" } }, "node_modules/@playwright/test": { - "version": "1.36.1", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.36.1.tgz", - "integrity": "sha512-YK7yGWK0N3C2QInPU6iaf/L3N95dlGdbsezLya4n0ZCh3IL7VgPGxC6Gnznh9ApWdOmkJeleT2kMTcWPRZvzqg==", + "version": "1.38.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.38.0.tgz", + "integrity": "sha512-xis/RXXsLxwThKnlIXouxmIvvT3zvQj1JE39GsNieMUrMpb3/GySHDh2j8itCG22qKVD4MYLBp7xB73cUW/UUw==", "dependencies": { - "@types/node": "*", - "playwright-core": "1.36.1" + "playwright": "1.38.0" }, "bin": { "playwright": "cli.js" }, "engines": { "node": ">=16" - }, - "optionalDependencies": { - "fsevents": "2.3.2" } }, "node_modules/@types/json-schema": { @@ -476,20 +508,21 @@ "dev": true }, "node_modules/@types/node": { - "version": "17.0.19", - "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.19.tgz", - "integrity": "sha512-PfeQhvcMR4cPFVuYfBN4ifG7p9c+Dlh3yUZR6k+5yQK7wX3gDgVxBly4/WkBRs9x4dmcy1TVl08SY67wwtEvmA==" + "version": "20.6.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.6.1.tgz", + "integrity": "sha512-4LcJvuXQlv4lTHnxwyHQZ3uR9Zw2j7m1C9DfuwoTFQQP4Pmu04O6IfLYgMmHoOCt0nosItLLZAH+sOrRE0Bo8g==", + "optional": true }, "node_modules/@types/semver": { - "version": "7.5.0", - "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.0.tgz", - "integrity": "sha512-G8hZ6XJiHnuhQKR7ZmysCeJWE08o8T0AXtk5darsCaTVsYZhhgUrq53jizaR2FvsoeCwJhlmwTjkXBY5Pn/ZHw==", + "version": "7.5.2", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.2.tgz", + "integrity": "sha512-7aqorHYgdNO4DM36stTiGO3DvKoex9TQRwsJU6vMaFGyqpBA1MNZkz+PG3gaNUPpTAOYhT1WR7M1JyA3fbS9Cw==", "dev": true }, "node_modules/@types/uuid": { - "version": "9.0.2", - "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.2.tgz", - "integrity": "sha512-kNnC1GFBLuhImSnV7w4njQkUiJi0ZXUycu1rUaouPqiKlXkh77JKgdRnTAp1x5eBwcIwbtI+3otwzuIDEuDoxQ==", + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.4.tgz", + "integrity": "sha512-zAuJWQflfx6dYJM62vna+Sn5aeSWhh3OB+wfUEACNcqUSc0AGc5JKl+ycL1vrH7frGTXhJchYjE1Hak8L819dA==", "dev": true }, "node_modules/@types/yauzl": { @@ -502,21 +535,20 @@ } }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.1.0.tgz", - "integrity": "sha512-qg7Bm5TyP/I7iilGyp6DRqqkt8na00lI6HbjWZObgk3FFSzH5ypRwAHXJhJkwiRtTcfn+xYQIMOR5kJgpo6upw==", + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.7.0.tgz", + "integrity": "sha512-gUqtknHm0TDs1LhY12K2NA3Rmlmp88jK9Tx8vGZMfHeNMLE3GH2e9TRub+y+SOjuYgtOmok+wt1AyDPZqxbNag==", "dev": true, "dependencies": { "@eslint-community/regexpp": "^4.5.1", - "@typescript-eslint/scope-manager": "6.1.0", - "@typescript-eslint/type-utils": "6.1.0", - "@typescript-eslint/utils": "6.1.0", - "@typescript-eslint/visitor-keys": "6.1.0", + "@typescript-eslint/scope-manager": "6.7.0", + "@typescript-eslint/type-utils": "6.7.0", + "@typescript-eslint/utils": "6.7.0", + "@typescript-eslint/visitor-keys": "6.7.0", "debug": "^4.3.4", "graphemer": "^1.4.0", "ignore": "^5.2.4", "natural-compare": "^1.4.0", - "natural-compare-lite": "^1.4.0", "semver": "^7.5.4", "ts-api-utils": "^1.0.1" }, @@ -538,15 +570,15 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.1.0.tgz", - "integrity": "sha512-hIzCPvX4vDs4qL07SYzyomamcs2/tQYXg5DtdAfj35AyJ5PIUqhsLf4YrEIFzZcND7R2E8tpQIZKayxg8/6Wbw==", + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.7.0.tgz", + "integrity": "sha512-jZKYwqNpNm5kzPVP5z1JXAuxjtl2uG+5NpaMocFPTNC2EdYIgbXIPImObOkhbONxtFTTdoZstLZefbaK+wXZng==", "dev": true, "dependencies": { - "@typescript-eslint/scope-manager": "6.1.0", - "@typescript-eslint/types": "6.1.0", - "@typescript-eslint/typescript-estree": "6.1.0", - "@typescript-eslint/visitor-keys": "6.1.0", + "@typescript-eslint/scope-manager": "6.7.0", + "@typescript-eslint/types": "6.7.0", + "@typescript-eslint/typescript-estree": "6.7.0", + "@typescript-eslint/visitor-keys": "6.7.0", "debug": "^4.3.4" }, "engines": { @@ -566,13 +598,13 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.1.0.tgz", - "integrity": "sha512-AxjgxDn27hgPpe2rQe19k0tXw84YCOsjDJ2r61cIebq1t+AIxbgiXKvD4999Wk49GVaAcdJ/d49FYel+Pp3jjw==", + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.7.0.tgz", + "integrity": "sha512-lAT1Uau20lQyjoLUQ5FUMSX/dS07qux9rYd5FGzKz/Kf8W8ccuvMyldb8hadHdK/qOI7aikvQWqulnEq2nCEYA==", "dev": true, "dependencies": { - "@typescript-eslint/types": "6.1.0", - "@typescript-eslint/visitor-keys": "6.1.0" + "@typescript-eslint/types": "6.7.0", + "@typescript-eslint/visitor-keys": "6.7.0" }, "engines": { "node": "^16.0.0 || >=18.0.0" @@ -583,13 +615,13 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.1.0.tgz", - "integrity": "sha512-kFXBx6QWS1ZZ5Ni89TyT1X9Ag6RXVIVhqDs0vZE/jUeWlBv/ixq2diua6G7ece6+fXw3TvNRxP77/5mOMusx2w==", + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.7.0.tgz", + "integrity": "sha512-f/QabJgDAlpSz3qduCyQT0Fw7hHpmhOzY/Rv6zO3yO+HVIdPfIWhrQoAyG+uZVtWAIS85zAyzgAFfyEr+MgBpg==", "dev": true, "dependencies": { - "@typescript-eslint/typescript-estree": "6.1.0", - "@typescript-eslint/utils": "6.1.0", + "@typescript-eslint/typescript-estree": "6.7.0", + "@typescript-eslint/utils": "6.7.0", "debug": "^4.3.4", "ts-api-utils": "^1.0.1" }, @@ -610,9 +642,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.1.0.tgz", - "integrity": "sha512-+Gfd5NHCpDoHDOaU/yIF3WWRI2PcBRKKpP91ZcVbL0t5tQpqYWBs3z/GGhvU+EV1D0262g9XCnyqQh19prU0JQ==", + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.7.0.tgz", + "integrity": "sha512-ihPfvOp7pOcN/ysoj0RpBPOx3HQTJTrIN8UZK+WFd3/iDeFHHqeyYxa4hQk4rMhsz9H9mXpR61IzwlBVGXtl9Q==", "dev": true, "engines": { "node": "^16.0.0 || >=18.0.0" @@ -623,13 +655,13 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.1.0.tgz", - "integrity": "sha512-nUKAPWOaP/tQjU1IQw9sOPCDavs/iU5iYLiY/6u7gxS7oKQoi4aUxXS1nrrVGTyBBaGesjkcwwHkbkiD5eBvcg==", + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.7.0.tgz", + "integrity": "sha512-dPvkXj3n6e9yd/0LfojNU8VMUGHWiLuBZvbM6V6QYD+2qxqInE7J+J/ieY2iGwR9ivf/R/haWGkIj04WVUeiSQ==", "dev": true, "dependencies": { - "@typescript-eslint/types": "6.1.0", - "@typescript-eslint/visitor-keys": "6.1.0", + "@typescript-eslint/types": "6.7.0", + "@typescript-eslint/visitor-keys": "6.7.0", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", @@ -650,17 +682,17 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.1.0.tgz", - "integrity": "sha512-wp652EogZlKmQoMS5hAvWqRKplXvkuOnNzZSE0PVvsKjpexd/XznRVHAtrfHFYmqaJz0DFkjlDsGYC9OXw+OhQ==", + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.7.0.tgz", + "integrity": "sha512-MfCq3cM0vh2slSikQYqK2Gq52gvOhe57vD2RM3V4gQRZYX4rDPnKLu5p6cm89+LJiGlwEXU8hkYxhqqEC/V3qA==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", "@types/json-schema": "^7.0.12", "@types/semver": "^7.5.0", - "@typescript-eslint/scope-manager": "6.1.0", - "@typescript-eslint/types": "6.1.0", - "@typescript-eslint/typescript-estree": "6.1.0", + "@typescript-eslint/scope-manager": "6.7.0", + "@typescript-eslint/types": "6.7.0", + "@typescript-eslint/typescript-estree": "6.7.0", "semver": "^7.5.4" }, "engines": { @@ -675,12 +707,12 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.1.0.tgz", - "integrity": "sha512-yQeh+EXhquh119Eis4k0kYhj9vmFzNpbhM3LftWQVwqVjipCkwHBQOZutcYW+JVkjtTG9k8nrZU1UoNedPDd1A==", + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.7.0.tgz", + "integrity": "sha512-/C1RVgKFDmGMcVGeD8HjKv2bd72oI1KxQDeY8uc66gw9R0OK0eMq48cA+jv9/2Ag6cdrsUGySm1yzYmfz0hxwQ==", "dev": true, "dependencies": { - "@typescript-eslint/types": "6.1.0", + "@typescript-eslint/types": "6.7.0", "eslint-visitor-keys": "^3.4.1" }, "engines": { @@ -776,9 +808,9 @@ "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" }, "node_modules/axe-core": { - "version": "4.7.2", - "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.7.2.tgz", - "integrity": "sha512-zIURGIS1E1Q4pcrMjp+nnEh+16G56eG/MUllJH8yEvw7asDo7Ac9uhC9KIH5jzpITueEZolfYglnCGIuSBz39g==", + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.8.1.tgz", + "integrity": "sha512-9l850jDDPnKq48nbad8SiEelCv4OrUWrKab/cPj0GScVg6cb6NbCCt/Ulk26QEq5jP9NnGr04Bit1BHyV6r5CQ==", "engines": { "node": ">=4" } @@ -929,13 +961,13 @@ } }, "node_modules/cosmiconfig": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.2.0.tgz", - "integrity": "sha512-3rTMnFJA1tCOPwRxtgF4wd7Ab2qvDbL8jX+3smjIbS4HlZBagTlpERbdN7iAbWlrfxE3M8c27kTwTawQ7st+OQ==", + "version": "8.3.6", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.3.6.tgz", + "integrity": "sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==", "dependencies": { - "import-fresh": "^3.2.1", + "import-fresh": "^3.3.0", "js-yaml": "^4.1.0", - "parse-json": "^5.0.0", + "parse-json": "^5.2.0", "path-type": "^4.0.0" }, "engines": { @@ -943,6 +975,14 @@ }, "funding": { "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "typescript": ">=4.9.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } } }, "node_modules/cross-env": { @@ -1078,27 +1118,27 @@ } }, "node_modules/eslint": { - "version": "8.45.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.45.0.tgz", - "integrity": "sha512-pd8KSxiQpdYRfYa9Wufvdoct3ZPQQuVuU5O6scNgMuOMYuxvH0IGaYK0wUFjo4UYYQQCUndlXiMbnxopwvvTiw==", + "version": "8.49.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.49.0.tgz", + "integrity": "sha512-jw03ENfm6VJI0jA9U+8H5zfl5b+FvuU3YYvZRdZHOlU2ggJkxrlkJH4HcDrZpj6YwD8kuYqvQM8LyesoazrSOQ==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.4.0", - "@eslint/eslintrc": "^2.1.0", - "@eslint/js": "8.44.0", - "@humanwhocodes/config-array": "^0.11.10", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.2", + "@eslint/js": "8.49.0", + "@humanwhocodes/config-array": "^0.11.11", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", - "ajv": "^6.10.0", + "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.2", "debug": "^4.3.2", "doctrine": "^3.0.0", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^7.2.0", - "eslint-visitor-keys": "^3.4.1", - "espree": "^9.6.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", "esquery": "^1.4.2", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", @@ -1132,9 +1172,9 @@ } }, "node_modules/eslint-scope": { - "version": "7.2.1", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.1.tgz", - "integrity": "sha512-CvefSOsDdaYYvxChovdrPo/ZGt8d5lrJWleAc1diXRKhHGiTYEI26cvo8Kle/wGnsizoCJjK73FMg1/IkIwiNA==", + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", "dev": true, "dependencies": { "esrecurse": "^4.3.0", @@ -1148,9 +1188,9 @@ } }, "node_modules/eslint-visitor-keys": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.1.tgz", - "integrity": "sha512-pZnmmLwYzf+kWaM/Qgrvpen51upAktaaiI01nsJD/Yr3lMOdNtq0cxkrrg16w64VtisN6okbs7Q8AfGqj4c9fA==", + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", "dev": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" @@ -1265,9 +1305,9 @@ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" }, "node_modules/fast-glob": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.0.tgz", - "integrity": "sha512-ChDuvbOypPuNjO8yIDf36x7BlZX1smcUMTTcyoIjycexOxd6DFsKsg21qVBzEmr3G7fUKIRy2/psii+CIUt7FA==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz", + "integrity": "sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==", "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", @@ -1486,9 +1526,9 @@ } }, "node_modules/globals": { - "version": "13.20.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.20.0.tgz", - "integrity": "sha512-Qg5QtVkCy/kv3FUSlu4ukeZDVf9ee0iXLAUYX13gbR17bnejFTzr4iS9bY7kwCf1NztRNm1t91fjOiyx4CSwPQ==", + "version": "13.21.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.21.0.tgz", + "integrity": "sha512-ybyme3s4yy/t/3s35bewwXKOf7cvzfreG2lH0lZl0JB7I4GxRP2ghxOK/Nb9EkRXdbBXZLfq/p/0W2JUONB/Gg==", "dev": true, "dependencies": { "type-fest": "^0.20.2" @@ -1819,12 +1859,6 @@ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "dev": true }, - "node_modules/natural-compare-lite": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz", - "integrity": "sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==", - "dev": true - }, "node_modules/node-domexception": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", @@ -1980,10 +2014,27 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/playwright": { + "version": "1.38.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.38.0.tgz", + "integrity": "sha512-fJGw+HO0YY+fU/F1N57DMO+TmXHTrmr905J05zwAQE9xkuwP/QLDk63rVhmyxh03dYnEhnRbsdbH9B0UVVRB3A==", + "dependencies": { + "playwright-core": "1.38.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=16" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, "node_modules/playwright-core": { - "version": "1.36.1", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.36.1.tgz", - "integrity": "sha512-7+tmPuMcEW4xeCL9cp9KxmYpQYHKkyjwoXRnoeTowaeNat8PoBMk/HwCYhqkH2fRkshfKEOiVus/IhID2Pg8kg==", + "version": "1.38.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.38.0.tgz", + "integrity": "sha512-f8z1y8J9zvmHoEhKgspmCvOExF2XdcxMW8jNRuX4vkQFrzV4MlZ55iwb5QeyiFQgOFCUolXiRHgpjSEnqvO48g==", "bin": { "playwright-core": "cli.js" }, @@ -2001,15 +2052,15 @@ } }, "node_modules/prettier": { - "version": "2.8.7", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.7.tgz", - "integrity": "sha512-yPngTo3aXUUmyuTjeTUT75txrf+aMh9FiD7q9ZE/i6r0bPb22g4FsE6Y338PQX1bmfy08i9QQCB7/rcUAVntfw==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.0.3.tgz", + "integrity": "sha512-L/4pUDMxcNa8R/EthV08Zt42WBO4h1rarVtK0K+QJG0X187OLo7l699jWw0GKuwzkPQ//jMFA/8Xm6Fh3J/DAg==", "dev": true, "bin": { - "prettier": "bin-prettier.js" + "prettier": "bin/prettier.cjs" }, "engines": { - "node": ">=10.13.0" + "node": ">=14" }, "funding": { "url": "https://github.com/prettier/prettier?sponsor=1" @@ -2271,16 +2322,16 @@ } }, "node_modules/typescript": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.0.4.tgz", - "integrity": "sha512-cW9T5W9xY37cc+jfEnaUvX91foxtHkza3Nw3wkoF4sSlKn0MONdkdEndig/qPBWXNkmplh3NzayQzCiHM4/hqw==", - "dev": true, + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz", + "integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==", + "devOptional": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" }, "engines": { - "node": ">=12.20" + "node": ">=14.17" } }, "node_modules/unfetch": { @@ -2297,9 +2348,13 @@ } }, "node_modules/uuid": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz", - "integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==", + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], "bin": { "uuid": "dist/bin/uuid" } @@ -2332,9 +2387,9 @@ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" }, "node_modules/ws": { - "version": "8.13.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.13.0.tgz", - "integrity": "sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==", + "version": "8.14.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.14.1.tgz", + "integrity": "sha512-4OOseMUq8AzRBI/7SLMUwO+FEDnguetSk7KMb1sHwvF2w2Wv5Hoj0nlifx8vtGsftE/jWHojPy8sMMzYLJ2G/A==", "engines": { "node": ">=10.0.0" }, @@ -2358,9 +2413,9 @@ "dev": true }, "node_modules/yaml": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.1.tgz", - "integrity": "sha512-2eHWfjaoXgTBC2jNM1LRef62VQa0umtvRiDSk6HSzW7RvS5YtkabJrwYLLEKWBc8a5U2PTSCs+dJjUTJdlHsWQ==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.2.tgz", + "integrity": "sha512-N/lyzTPaJasoDmfV7YTrYCI0G/3ivm/9wdG0aHuheKowWQwGTsK0Eoiw6utmzAnI6pkJa0DUVygvp3spqqEKXg==", "engines": { "node": ">= 14" } @@ -2403,25 +2458,43 @@ } }, "@babel/code-frame": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.5.tgz", - "integrity": "sha512-Xmwn266vad+6DAqEB2A6V/CcZVp62BbwVmcOJc2RPuwih1kw02TjQvWVWlcKGbBPd+8/0V5DEkOcizRGYsspYQ==", + "version": "7.22.13", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz", + "integrity": "sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==", "requires": { - "@babel/highlight": "^7.22.5" + "@babel/highlight": "^7.22.13", + "chalk": "^2.4.2" + }, + "dependencies": { + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==" + } } }, "@babel/helper-validator-identifier": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.5.tgz", - "integrity": "sha512-aJXu+6lErq8ltp+JhkJUfk1MTGyuA4v7f3pA+BJ5HLfNC6nAQ0Cpi9uOquUj8Hehg0aUiHzWQbOVJGao6ztBAQ==" + "version": "7.22.19", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.19.tgz", + "integrity": "sha512-Tinq7ybnEPFFXhlYOYFiSjespWQk0dq2dRNAiMdRTOYQzEGqnnNyrTxPYHP5r6wGjlF1rFgABdDV0g8EwD6Qbg==" }, "@babel/highlight": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.5.tgz", - "integrity": "sha512-BSKlD1hgnedS5XRnGOljZawtag7H1yPfQp0tdNJCHoH6AZ+Pcm9VvkrK59/Yy593Ypg0zMxH2BxD1VPYUQ7UIw==", + "version": "7.22.13", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.13.tgz", + "integrity": "sha512-C/BaXcnnvBCmHTpz/VGZ8jgtE2aYlW4hxDhseJAWZb7gqGM/qtCK6iZUb0TyKFf7BOUsBH7Q7fkRsDRhg1XklQ==", "requires": { "@babel/helper-validator-identifier": "^7.22.5", - "chalk": "^2.0.0", + "chalk": "^2.4.2", "js-tokens": "^4.0.0" }, "dependencies": { @@ -2452,15 +2525,15 @@ } }, "@eslint-community/regexpp": { - "version": "4.5.1", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.5.1.tgz", - "integrity": "sha512-Z5ba73P98O1KUYCCJTUeVpja9RcGoMdncZ6T49FCUl2lN38JtCJ+3WgIDBv0AuY4WChU5PmtJmOCTlN6FZTFKQ==", + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.8.1.tgz", + "integrity": "sha512-PWiOzLIUAjN/w5K17PoF4n6sKBw0gqLHPhywmYHP4t1VFQQVYeb1yWsJwnMVEMl3tUHME7X/SJPZLmtG7XBDxQ==", "dev": true }, "@eslint/eslintrc": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.0.tgz", - "integrity": "sha512-Lj7DECXqIVCqnqjjHMPna4vn6GJcMgul/wuS0je9OZ9gsL0zzDpKPVtcG1HaDVc+9y+qgXneTeUMbCqXJNpH1A==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.2.tgz", + "integrity": "sha512-+wvgpDsrB1YqAMdEUCcnTlpfVBH7Vqn6A/NT3D8WVXFIaKMlErPIZT3oCIAVCOtarRpMtelZLqJeU3t7WY6X6g==", "dev": true, "requires": { "ajv": "^6.12.4", @@ -2495,15 +2568,15 @@ } }, "@eslint/js": { - "version": "8.44.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.44.0.tgz", - "integrity": "sha512-Ag+9YM4ocKQx9AarydN0KY2j0ErMHNIocPDrVo8zAE44xLTjEtz81OdR68/cydGtk6m6jDb5Za3r2useMzYmSw==", + "version": "8.49.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.49.0.tgz", + "integrity": "sha512-1S8uAY/MTJqVx0SC4epBq+N2yhuwtNwLbJYNZyhL2pO1ZVKn5HFXav5T41Ryzy9K9V7ZId2JB2oy/W4aCd9/2w==", "dev": true }, "@humanwhocodes/config-array": { - "version": "0.11.10", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.10.tgz", - "integrity": "sha512-KVVjQmNUepDVGXNuoRRdmmEjruj0KfiGSbS8LVc12LMsWDQzRXJ0qdhN8L8uUigKpfEHRhlaQFY0ib1tnUbNeQ==", + "version": "0.11.11", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.11.tgz", + "integrity": "sha512-N2brEuAadi0CcdeMXUkhbZB84eskAc8MEX1By6qEchoVywSgXPIjou4rYsl0V3Hj0ZnuGycGCjdNgockbzeWNA==", "dev": true, "requires": { "@humanwhocodes/object-schema": "^1.2.1", @@ -2547,114 +2620,115 @@ } }, "@percy/cli": { - "version": "1.26.2", - "resolved": "https://registry.npmjs.org/@percy/cli/-/cli-1.26.2.tgz", - "integrity": "sha512-RKuylqXNfVAO+3XpUpFh67vnjB/tvSh98aDuEf1z8CNuTBSYA7YJbB5lxOnEcQ3DRMaQuemZ1zX0UyiHxLnjEw==", + "version": "1.27.1", + "resolved": "https://registry.npmjs.org/@percy/cli/-/cli-1.27.1.tgz", + "integrity": "sha512-2rU4NFe8TshV2sF+fs8bseGNohpLuRILW4t/Is7PCFYAitla1nvO0mndy0INct5VWc2KKewnq+2ZQONx3iNXLw==", "requires": { - "@percy/cli-app": "1.26.2", - "@percy/cli-build": "1.26.2", - "@percy/cli-command": "1.26.2", - "@percy/cli-config": "1.26.2", - "@percy/cli-exec": "1.26.2", - "@percy/cli-snapshot": "1.26.2", - "@percy/cli-upload": "1.26.2", - "@percy/client": "1.26.2", - "@percy/logger": "1.26.2" + "@percy/cli-app": "1.27.1", + "@percy/cli-build": "1.27.1", + "@percy/cli-command": "1.27.1", + "@percy/cli-config": "1.27.1", + "@percy/cli-exec": "1.27.1", + "@percy/cli-snapshot": "1.27.1", + "@percy/cli-upload": "1.27.1", + "@percy/client": "1.27.1", + "@percy/logger": "1.27.1" } }, "@percy/cli-app": { - "version": "1.26.2", - "resolved": "https://registry.npmjs.org/@percy/cli-app/-/cli-app-1.26.2.tgz", - "integrity": "sha512-eOg1vs6J3eINREf4SWt7YO9KJwGru2JPOXlV23fZgLaSw1iEaZqpa1NmeMlSX2NsU+J8JdAox1hkmO884QHE8A==", + "version": "1.27.1", + "resolved": "https://registry.npmjs.org/@percy/cli-app/-/cli-app-1.27.1.tgz", + "integrity": "sha512-Pat2BogPUmyciURUWbI1PQtUdPlhUrnS6aEVO1gNrulDeYF6j4LgD4SycCTj5L/xJijMXa3qFkZwW1VSdx97Yg==", "requires": { - "@percy/cli-command": "1.26.2", - "@percy/cli-exec": "1.26.2" + "@percy/cli-command": "1.27.1", + "@percy/cli-exec": "1.27.1" } }, "@percy/cli-build": { - "version": "1.26.2", - "resolved": "https://registry.npmjs.org/@percy/cli-build/-/cli-build-1.26.2.tgz", - "integrity": "sha512-VJJcLfWmKhVy7WHojpPtslRA8rm77umh/f7kr3kcYE2HVXDVRcH8r72zClglhYdLcrIfb/3BUCX6JXxJfgM7tg==", + "version": "1.27.1", + "resolved": "https://registry.npmjs.org/@percy/cli-build/-/cli-build-1.27.1.tgz", + "integrity": "sha512-EtktlYj4kzsBkrBSOVLKrvZWew/nq9mw/Z2m8mU4Jf7Pc779rQGUVCuI8PBhOI59c3JTjnY8KVaaj4Y+z9X+fg==", "requires": { - "@percy/cli-command": "1.26.2" + "@percy/cli-command": "1.27.1" } }, "@percy/cli-command": { - "version": "1.26.2", - "resolved": "https://registry.npmjs.org/@percy/cli-command/-/cli-command-1.26.2.tgz", - "integrity": "sha512-G5qALrrprUO5Zc3TrEPzqys6+clWtXjHGWVcHYMaBY0i7xwH4D0FaqtzkGRjiUxbsb353qV35B48oKsm0FJDmg==", + "version": "1.27.1", + "resolved": "https://registry.npmjs.org/@percy/cli-command/-/cli-command-1.27.1.tgz", + "integrity": "sha512-LK9YwE7qr67EtShVVjc20aJaRF8BFjZ8VWneLVoN73IYgpwBq96J4AUomoqfRpPNkpKvv6wDZFpr30xFokbrHA==", "requires": { - "@percy/config": "1.26.2", - "@percy/core": "1.26.2", - "@percy/logger": "1.26.2" + "@percy/config": "1.27.1", + "@percy/core": "1.27.1", + "@percy/logger": "1.27.1" } }, "@percy/cli-config": { - "version": "1.26.2", - "resolved": "https://registry.npmjs.org/@percy/cli-config/-/cli-config-1.26.2.tgz", - "integrity": "sha512-E/zb2Eg1HK8JDzloY0FdguZS0nE47QwZoW9uxMw8QD2wykxIkbQeJq8Jl5a48BJ3rTj8TnKMzyTo7yMALqnISw==", + "version": "1.27.1", + "resolved": "https://registry.npmjs.org/@percy/cli-config/-/cli-config-1.27.1.tgz", + "integrity": "sha512-bEzXfdP+RRxHekZ5pULsuuKHL0K9E81IFcEqZ8DcE3gnrpWQnIFPP2fTHF6WgWuXYHzJnsoIIm90LaWYc4+BJQ==", "requires": { - "@percy/cli-command": "1.26.2" + "@percy/cli-command": "1.27.1" } }, "@percy/cli-exec": { - "version": "1.26.2", - "resolved": "https://registry.npmjs.org/@percy/cli-exec/-/cli-exec-1.26.2.tgz", - "integrity": "sha512-7gMzZshrtPbxHLkiduA7OcCqbhxwAZ17x3C8XdFYwzvW3MpghKhSpwK24hGH3169nvnoRjf5GzG7CHIBVaEndQ==", + "version": "1.27.1", + "resolved": "https://registry.npmjs.org/@percy/cli-exec/-/cli-exec-1.27.1.tgz", + "integrity": "sha512-8xB7Iq3fh+QfuViUeZQXZeJNoFzDf0IhYUhhlEDXPcL2sgjrLiy9QHBdq6Yn3/8gHlDJyzpkej6hzFqERYZs1g==", "requires": { - "@percy/cli-command": "1.26.2", + "@percy/cli-command": "1.27.1", "cross-spawn": "^7.0.3", "which": "^2.0.2" } }, "@percy/cli-snapshot": { - "version": "1.26.2", - "resolved": "https://registry.npmjs.org/@percy/cli-snapshot/-/cli-snapshot-1.26.2.tgz", - "integrity": "sha512-R0rtuRkxzOfjr6N2XQyPBmL+zuXvbmNqjuBMDo8bmPy+j+b16TWFn25Q6r3jKLPdRkXVXrAMFsEsp9um1tLVrQ==", + "version": "1.27.1", + "resolved": "https://registry.npmjs.org/@percy/cli-snapshot/-/cli-snapshot-1.27.1.tgz", + "integrity": "sha512-hVYATkHSN6Mz67LuP5QqRvdblg+kLb9dqGlaMJZI0CtuQYRtkbrlPz5yII9SJGMyUYMQHbZKsc68eT7+LZHKbA==", "requires": { - "@percy/cli-command": "1.26.2", + "@percy/cli-command": "1.27.1", "yaml": "^2.0.0" } }, "@percy/cli-upload": { - "version": "1.26.2", - "resolved": "https://registry.npmjs.org/@percy/cli-upload/-/cli-upload-1.26.2.tgz", - "integrity": "sha512-10dJn3paz1jFQaNf59j0Kt6pkz1BDjlSstfS5f74Lwi8g3tFXLv3M/B25bOdDxLrToX1Dg8ke71wyIGTCqeLjg==", + "version": "1.27.1", + "resolved": "https://registry.npmjs.org/@percy/cli-upload/-/cli-upload-1.27.1.tgz", + "integrity": "sha512-4Kcft6ceuWy+Q5T4PKJXI63/QxCRFtCJUoU0QYrWo6TKKsescdE7/zWy6YESqHIm+XzBhZFYHwdYRsIbnjqUqQ==", "requires": { - "@percy/cli-command": "1.26.2", + "@percy/cli-command": "1.27.1", "fast-glob": "^3.2.11", "image-size": "^1.0.0" } }, "@percy/client": { - "version": "1.26.2", - "resolved": "https://registry.npmjs.org/@percy/client/-/client-1.26.2.tgz", - "integrity": "sha512-0aJtwiL8Pt06O+4nHGBoXU2uaJmYR+Df846HCpJmKhMuS5X/OKZgd2DiOlRs61JPwP+FuHFZleUvrhngyyJrzQ==", + "version": "1.27.1", + "resolved": "https://registry.npmjs.org/@percy/client/-/client-1.27.1.tgz", + "integrity": "sha512-GvjrGUaVdjMwx8ODDja3Kdb6tXYxRxv4PLXfXg0Wbn5jIfcjicojOc0mSGxcLvRGu5tmKrE3fusMhYYtKlLihg==", "requires": { - "@percy/env": "1.26.2", - "@percy/logger": "1.26.2" + "@percy/env": "1.27.1", + "@percy/logger": "1.27.1" } }, "@percy/config": { - "version": "1.26.2", - "resolved": "https://registry.npmjs.org/@percy/config/-/config-1.26.2.tgz", - "integrity": "sha512-x0Y0I0m6Kcluzt/CPQzNITKYGNboWBqTa879tSxTIayk3sR7omjjQdPgNpVpwDwcx05h2sR1eFz6Plq+EF55cA==", + "version": "1.27.1", + "resolved": "https://registry.npmjs.org/@percy/config/-/config-1.27.1.tgz", + "integrity": "sha512-O7GgSrpjPeRmUmPWpFrg7368lWTJj8BtKY18Ztpq2j7bef5HCFi0AxvuOjwIH/GEwzGm/cI7ZK2kA/XGTOtpsg==", "requires": { - "@percy/logger": "1.26.2", + "@percy/logger": "1.27.1", "ajv": "^8.6.2", "cosmiconfig": "^8.0.0", "yaml": "^2.0.0" } }, "@percy/core": { - "version": "1.26.2", - "resolved": "https://registry.npmjs.org/@percy/core/-/core-1.26.2.tgz", - "integrity": "sha512-cULZaGOH0xSgA5asRpuGUzcKBVItLLFJ2jhZn60n+8ueKqXpbSe7ggNKzaMoSVvNh6T5Kd8ZMYAzdg+Ijf6qeg==", + "version": "1.27.1", + "resolved": "https://registry.npmjs.org/@percy/core/-/core-1.27.1.tgz", + "integrity": "sha512-/XsOCk/XHYf9MAqcituMGrYeITiav9BW1oGM0LfmU1Gne/YnEvur4JwpJ4uJ0OrNennwIqMuAVM9Y5y/xfZ39w==", "requires": { - "@percy/client": "1.26.2", - "@percy/config": "1.26.2", - "@percy/dom": "1.26.2", - "@percy/logger": "1.26.2", + "@percy/client": "1.27.1", + "@percy/config": "1.27.1", + "@percy/dom": "1.27.1", + "@percy/logger": "1.27.1", + "@percy/webdriver-utils": "1.27.1", "content-disposition": "^0.5.4", "cross-spawn": "^7.0.3", "extract-zip": "^2.0.1", @@ -2667,22 +2741,22 @@ } }, "@percy/dom": { - "version": "1.26.2", - "resolved": "https://registry.npmjs.org/@percy/dom/-/dom-1.26.2.tgz", - "integrity": "sha512-LaB5yNk3Ds2ouOXI5YtszJY8f+Uj61fyOknOE9zWF57rSfOck92uK0DcgVtBMowQTzTIzdHn+i4ZnIBQ+4ex8g==" + "version": "1.27.1", + "resolved": "https://registry.npmjs.org/@percy/dom/-/dom-1.27.1.tgz", + "integrity": "sha512-duYA3ATjADtn05VuGIfezgdCq+8ASwcY6Mzk857DzFIlJd/6T2P8v1dy66RRp78efzmIMtRroGy9SqWonlaAEA==" }, "@percy/env": { - "version": "1.26.2", - "resolved": "https://registry.npmjs.org/@percy/env/-/env-1.26.2.tgz", - "integrity": "sha512-aKSN1r3G/evQTc5aAEheSU5oq2sJVWcZvIqiJTKfmw2K+91R6VLEUWMemMPHSAAyi2GCrV2D2JEpFtFWEeXQyQ==", + "version": "1.27.1", + "resolved": "https://registry.npmjs.org/@percy/env/-/env-1.27.1.tgz", + "integrity": "sha512-Og0vQfV9zJftYYOY3PVtu+r7Ut/xr72BP3jH3rkeQJHnFKLkwemGbJpgSpMz7IbzWXSxYONkNfUtLvXwEsRpAw==", "requires": { - "@percy/logger": "1.26.2" + "@percy/logger": "1.27.1" } }, "@percy/logger": { - "version": "1.26.2", - "resolved": "https://registry.npmjs.org/@percy/logger/-/logger-1.26.2.tgz", - "integrity": "sha512-EVwPoUJWm2ESn3Llorc6D3mqwQeALgaDerlsXA7JvKOk2Xaf68CRk5akI8NBN6rZoWIx8t5WjjJksGzPvMYYnw==" + "version": "1.27.1", + "resolved": "https://registry.npmjs.org/@percy/logger/-/logger-1.27.1.tgz", + "integrity": "sha512-jH++paKzIY94fQYQBEedVTfO36I8WBqm2RNl/xzsnLuAwNEX5+JUsaPcbLrwRzhn/tCv+8h9xEBuE9nj23krIA==" }, "@percy/playwright": { "version": "1.0.4", @@ -2693,18 +2767,25 @@ } }, "@percy/sdk-utils": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@percy/sdk-utils/-/sdk-utils-1.6.0.tgz", - "integrity": "sha512-4cSkWqpu7uK9zzeVwtMWrgGbP34GUlvZsWdEEt98ep6ZECQA+iGB75pOpIVwcsHKXtkDRE6fgugtxNXs5uHpMg==" + "version": "1.27.1", + "resolved": "https://registry.npmjs.org/@percy/sdk-utils/-/sdk-utils-1.27.1.tgz", + "integrity": "sha512-I12rBQYySyt8VILgYnI19obYtkaIPuFR07HVBrHLsRHiLUf92XzAtTI482qrnYwybCgU9mb9o3Kb7KtMJ0nalA==" + }, + "@percy/webdriver-utils": { + "version": "1.27.1", + "resolved": "https://registry.npmjs.org/@percy/webdriver-utils/-/webdriver-utils-1.27.1.tgz", + "integrity": "sha512-bPj16YmvV84egtMHyYEhlGYjwJTk0f6vopcbCYWgMgvQp78Y/RKwRm8ZpWZfW2rTqElNRudrhKdqcEeaG6qd9g==", + "requires": { + "@percy/config": "1.27.1", + "@percy/sdk-utils": "1.27.1" + } }, "@playwright/test": { - "version": "1.36.1", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.36.1.tgz", - "integrity": "sha512-YK7yGWK0N3C2QInPU6iaf/L3N95dlGdbsezLya4n0ZCh3IL7VgPGxC6Gnznh9ApWdOmkJeleT2kMTcWPRZvzqg==", + "version": "1.38.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.38.0.tgz", + "integrity": "sha512-xis/RXXsLxwThKnlIXouxmIvvT3zvQj1JE39GsNieMUrMpb3/GySHDh2j8itCG22qKVD4MYLBp7xB73cUW/UUw==", "requires": { - "@types/node": "*", - "fsevents": "2.3.2", - "playwright-core": "1.36.1" + "playwright": "1.38.0" } }, "@types/json-schema": { @@ -2714,20 +2795,21 @@ "dev": true }, "@types/node": { - "version": "17.0.19", - "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.19.tgz", - "integrity": "sha512-PfeQhvcMR4cPFVuYfBN4ifG7p9c+Dlh3yUZR6k+5yQK7wX3gDgVxBly4/WkBRs9x4dmcy1TVl08SY67wwtEvmA==" + "version": "20.6.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.6.1.tgz", + "integrity": "sha512-4LcJvuXQlv4lTHnxwyHQZ3uR9Zw2j7m1C9DfuwoTFQQP4Pmu04O6IfLYgMmHoOCt0nosItLLZAH+sOrRE0Bo8g==", + "optional": true }, "@types/semver": { - "version": "7.5.0", - "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.0.tgz", - "integrity": "sha512-G8hZ6XJiHnuhQKR7ZmysCeJWE08o8T0AXtk5darsCaTVsYZhhgUrq53jizaR2FvsoeCwJhlmwTjkXBY5Pn/ZHw==", + "version": "7.5.2", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.2.tgz", + "integrity": "sha512-7aqorHYgdNO4DM36stTiGO3DvKoex9TQRwsJU6vMaFGyqpBA1MNZkz+PG3gaNUPpTAOYhT1WR7M1JyA3fbS9Cw==", "dev": true }, "@types/uuid": { - "version": "9.0.2", - "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.2.tgz", - "integrity": "sha512-kNnC1GFBLuhImSnV7w4njQkUiJi0ZXUycu1rUaouPqiKlXkh77JKgdRnTAp1x5eBwcIwbtI+3otwzuIDEuDoxQ==", + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.4.tgz", + "integrity": "sha512-zAuJWQflfx6dYJM62vna+Sn5aeSWhh3OB+wfUEACNcqUSc0AGc5JKl+ycL1vrH7frGTXhJchYjE1Hak8L819dA==", "dev": true }, "@types/yauzl": { @@ -2740,74 +2822,73 @@ } }, "@typescript-eslint/eslint-plugin": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.1.0.tgz", - "integrity": "sha512-qg7Bm5TyP/I7iilGyp6DRqqkt8na00lI6HbjWZObgk3FFSzH5ypRwAHXJhJkwiRtTcfn+xYQIMOR5kJgpo6upw==", + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.7.0.tgz", + "integrity": "sha512-gUqtknHm0TDs1LhY12K2NA3Rmlmp88jK9Tx8vGZMfHeNMLE3GH2e9TRub+y+SOjuYgtOmok+wt1AyDPZqxbNag==", "dev": true, "requires": { "@eslint-community/regexpp": "^4.5.1", - "@typescript-eslint/scope-manager": "6.1.0", - "@typescript-eslint/type-utils": "6.1.0", - "@typescript-eslint/utils": "6.1.0", - "@typescript-eslint/visitor-keys": "6.1.0", + "@typescript-eslint/scope-manager": "6.7.0", + "@typescript-eslint/type-utils": "6.7.0", + "@typescript-eslint/utils": "6.7.0", + "@typescript-eslint/visitor-keys": "6.7.0", "debug": "^4.3.4", "graphemer": "^1.4.0", "ignore": "^5.2.4", "natural-compare": "^1.4.0", - "natural-compare-lite": "^1.4.0", "semver": "^7.5.4", "ts-api-utils": "^1.0.1" } }, "@typescript-eslint/parser": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.1.0.tgz", - "integrity": "sha512-hIzCPvX4vDs4qL07SYzyomamcs2/tQYXg5DtdAfj35AyJ5PIUqhsLf4YrEIFzZcND7R2E8tpQIZKayxg8/6Wbw==", + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.7.0.tgz", + "integrity": "sha512-jZKYwqNpNm5kzPVP5z1JXAuxjtl2uG+5NpaMocFPTNC2EdYIgbXIPImObOkhbONxtFTTdoZstLZefbaK+wXZng==", "dev": true, "requires": { - "@typescript-eslint/scope-manager": "6.1.0", - "@typescript-eslint/types": "6.1.0", - "@typescript-eslint/typescript-estree": "6.1.0", - "@typescript-eslint/visitor-keys": "6.1.0", + "@typescript-eslint/scope-manager": "6.7.0", + "@typescript-eslint/types": "6.7.0", + "@typescript-eslint/typescript-estree": "6.7.0", + "@typescript-eslint/visitor-keys": "6.7.0", "debug": "^4.3.4" } }, "@typescript-eslint/scope-manager": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.1.0.tgz", - "integrity": "sha512-AxjgxDn27hgPpe2rQe19k0tXw84YCOsjDJ2r61cIebq1t+AIxbgiXKvD4999Wk49GVaAcdJ/d49FYel+Pp3jjw==", + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.7.0.tgz", + "integrity": "sha512-lAT1Uau20lQyjoLUQ5FUMSX/dS07qux9rYd5FGzKz/Kf8W8ccuvMyldb8hadHdK/qOI7aikvQWqulnEq2nCEYA==", "dev": true, "requires": { - "@typescript-eslint/types": "6.1.0", - "@typescript-eslint/visitor-keys": "6.1.0" + "@typescript-eslint/types": "6.7.0", + "@typescript-eslint/visitor-keys": "6.7.0" } }, "@typescript-eslint/type-utils": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.1.0.tgz", - "integrity": "sha512-kFXBx6QWS1ZZ5Ni89TyT1X9Ag6RXVIVhqDs0vZE/jUeWlBv/ixq2diua6G7ece6+fXw3TvNRxP77/5mOMusx2w==", + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.7.0.tgz", + "integrity": "sha512-f/QabJgDAlpSz3qduCyQT0Fw7hHpmhOzY/Rv6zO3yO+HVIdPfIWhrQoAyG+uZVtWAIS85zAyzgAFfyEr+MgBpg==", "dev": true, "requires": { - "@typescript-eslint/typescript-estree": "6.1.0", - "@typescript-eslint/utils": "6.1.0", + "@typescript-eslint/typescript-estree": "6.7.0", + "@typescript-eslint/utils": "6.7.0", "debug": "^4.3.4", "ts-api-utils": "^1.0.1" } }, "@typescript-eslint/types": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.1.0.tgz", - "integrity": "sha512-+Gfd5NHCpDoHDOaU/yIF3WWRI2PcBRKKpP91ZcVbL0t5tQpqYWBs3z/GGhvU+EV1D0262g9XCnyqQh19prU0JQ==", + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.7.0.tgz", + "integrity": "sha512-ihPfvOp7pOcN/ysoj0RpBPOx3HQTJTrIN8UZK+WFd3/iDeFHHqeyYxa4hQk4rMhsz9H9mXpR61IzwlBVGXtl9Q==", "dev": true }, "@typescript-eslint/typescript-estree": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.1.0.tgz", - "integrity": "sha512-nUKAPWOaP/tQjU1IQw9sOPCDavs/iU5iYLiY/6u7gxS7oKQoi4aUxXS1nrrVGTyBBaGesjkcwwHkbkiD5eBvcg==", + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.7.0.tgz", + "integrity": "sha512-dPvkXj3n6e9yd/0LfojNU8VMUGHWiLuBZvbM6V6QYD+2qxqInE7J+J/ieY2iGwR9ivf/R/haWGkIj04WVUeiSQ==", "dev": true, "requires": { - "@typescript-eslint/types": "6.1.0", - "@typescript-eslint/visitor-keys": "6.1.0", + "@typescript-eslint/types": "6.7.0", + "@typescript-eslint/visitor-keys": "6.7.0", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", @@ -2816,27 +2897,27 @@ } }, "@typescript-eslint/utils": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.1.0.tgz", - "integrity": "sha512-wp652EogZlKmQoMS5hAvWqRKplXvkuOnNzZSE0PVvsKjpexd/XznRVHAtrfHFYmqaJz0DFkjlDsGYC9OXw+OhQ==", + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.7.0.tgz", + "integrity": "sha512-MfCq3cM0vh2slSikQYqK2Gq52gvOhe57vD2RM3V4gQRZYX4rDPnKLu5p6cm89+LJiGlwEXU8hkYxhqqEC/V3qA==", "dev": true, "requires": { "@eslint-community/eslint-utils": "^4.4.0", "@types/json-schema": "^7.0.12", "@types/semver": "^7.5.0", - "@typescript-eslint/scope-manager": "6.1.0", - "@typescript-eslint/types": "6.1.0", - "@typescript-eslint/typescript-estree": "6.1.0", + "@typescript-eslint/scope-manager": "6.7.0", + "@typescript-eslint/types": "6.7.0", + "@typescript-eslint/typescript-estree": "6.7.0", "semver": "^7.5.4" } }, "@typescript-eslint/visitor-keys": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.1.0.tgz", - "integrity": "sha512-yQeh+EXhquh119Eis4k0kYhj9vmFzNpbhM3LftWQVwqVjipCkwHBQOZutcYW+JVkjtTG9k8nrZU1UoNedPDd1A==", + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.7.0.tgz", + "integrity": "sha512-/C1RVgKFDmGMcVGeD8HjKv2bd72oI1KxQDeY8uc66gw9R0OK0eMq48cA+jv9/2Ag6cdrsUGySm1yzYmfz0hxwQ==", "dev": true, "requires": { - "@typescript-eslint/types": "6.1.0", + "@typescript-eslint/types": "6.7.0", "eslint-visitor-keys": "^3.4.1" } }, @@ -2900,9 +2981,9 @@ "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" }, "axe-core": { - "version": "4.7.2", - "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.7.2.tgz", - "integrity": "sha512-zIURGIS1E1Q4pcrMjp+nnEh+16G56eG/MUllJH8yEvw7asDo7Ac9uhC9KIH5jzpITueEZolfYglnCGIuSBz39g==" + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.8.1.tgz", + "integrity": "sha512-9l850jDDPnKq48nbad8SiEelCv4OrUWrKab/cPj0GScVg6cb6NbCCt/Ulk26QEq5jP9NnGr04Bit1BHyV6r5CQ==" }, "balanced-match": { "version": "1.0.2", @@ -3016,13 +3097,13 @@ } }, "cosmiconfig": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.2.0.tgz", - "integrity": "sha512-3rTMnFJA1tCOPwRxtgF4wd7Ab2qvDbL8jX+3smjIbS4HlZBagTlpERbdN7iAbWlrfxE3M8c27kTwTawQ7st+OQ==", + "version": "8.3.6", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.3.6.tgz", + "integrity": "sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==", "requires": { - "import-fresh": "^3.2.1", + "import-fresh": "^3.3.0", "js-yaml": "^4.1.0", - "parse-json": "^5.0.0", + "parse-json": "^5.2.0", "path-type": "^4.0.0" } }, @@ -3115,27 +3196,27 @@ "dev": true }, "eslint": { - "version": "8.45.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.45.0.tgz", - "integrity": "sha512-pd8KSxiQpdYRfYa9Wufvdoct3ZPQQuVuU5O6scNgMuOMYuxvH0IGaYK0wUFjo4UYYQQCUndlXiMbnxopwvvTiw==", + "version": "8.49.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.49.0.tgz", + "integrity": "sha512-jw03ENfm6VJI0jA9U+8H5zfl5b+FvuU3YYvZRdZHOlU2ggJkxrlkJH4HcDrZpj6YwD8kuYqvQM8LyesoazrSOQ==", "dev": true, "requires": { "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.4.0", - "@eslint/eslintrc": "^2.1.0", - "@eslint/js": "8.44.0", - "@humanwhocodes/config-array": "^0.11.10", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.2", + "@eslint/js": "8.49.0", + "@humanwhocodes/config-array": "^0.11.11", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", - "ajv": "^6.10.0", + "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.2", "debug": "^4.3.2", "doctrine": "^3.0.0", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^7.2.0", - "eslint-visitor-keys": "^3.4.1", - "espree": "^9.6.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", "esquery": "^1.4.2", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", @@ -3180,9 +3261,9 @@ } }, "eslint-scope": { - "version": "7.2.1", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.1.tgz", - "integrity": "sha512-CvefSOsDdaYYvxChovdrPo/ZGt8d5lrJWleAc1diXRKhHGiTYEI26cvo8Kle/wGnsizoCJjK73FMg1/IkIwiNA==", + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", "dev": true, "requires": { "esrecurse": "^4.3.0", @@ -3190,9 +3271,9 @@ } }, "eslint-visitor-keys": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.1.tgz", - "integrity": "sha512-pZnmmLwYzf+kWaM/Qgrvpen51upAktaaiI01nsJD/Yr3lMOdNtq0cxkrrg16w64VtisN6okbs7Q8AfGqj4c9fA==", + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", "dev": true }, "espree": { @@ -3253,9 +3334,9 @@ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" }, "fast-glob": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.0.tgz", - "integrity": "sha512-ChDuvbOypPuNjO8yIDf36x7BlZX1smcUMTTcyoIjycexOxd6DFsKsg21qVBzEmr3G7fUKIRy2/psii+CIUt7FA==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz", + "integrity": "sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==", "requires": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", @@ -3414,9 +3495,9 @@ } }, "globals": { - "version": "13.20.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.20.0.tgz", - "integrity": "sha512-Qg5QtVkCy/kv3FUSlu4ukeZDVf9ee0iXLAUYX13gbR17bnejFTzr4iS9bY7kwCf1NztRNm1t91fjOiyx4CSwPQ==", + "version": "13.21.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.21.0.tgz", + "integrity": "sha512-ybyme3s4yy/t/3s35bewwXKOf7cvzfreG2lH0lZl0JB7I4GxRP2ghxOK/Nb9EkRXdbBXZLfq/p/0W2JUONB/Gg==", "dev": true, "requires": { "type-fest": "^0.20.2" @@ -3664,12 +3745,6 @@ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "dev": true }, - "natural-compare-lite": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz", - "integrity": "sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==", - "dev": true - }, "node-domexception": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", @@ -3770,10 +3845,19 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==" }, + "playwright": { + "version": "1.38.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.38.0.tgz", + "integrity": "sha512-fJGw+HO0YY+fU/F1N57DMO+TmXHTrmr905J05zwAQE9xkuwP/QLDk63rVhmyxh03dYnEhnRbsdbH9B0UVVRB3A==", + "requires": { + "fsevents": "2.3.2", + "playwright-core": "1.38.0" + } + }, "playwright-core": { - "version": "1.36.1", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.36.1.tgz", - "integrity": "sha512-7+tmPuMcEW4xeCL9cp9KxmYpQYHKkyjwoXRnoeTowaeNat8PoBMk/HwCYhqkH2fRkshfKEOiVus/IhID2Pg8kg==" + "version": "1.38.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.38.0.tgz", + "integrity": "sha512-f8z1y8J9zvmHoEhKgspmCvOExF2XdcxMW8jNRuX4vkQFrzV4MlZ55iwb5QeyiFQgOFCUolXiRHgpjSEnqvO48g==" }, "prelude-ls": { "version": "1.2.1", @@ -3782,9 +3866,9 @@ "dev": true }, "prettier": { - "version": "2.8.7", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.7.tgz", - "integrity": "sha512-yPngTo3aXUUmyuTjeTUT75txrf+aMh9FiD7q9ZE/i6r0bPb22g4FsE6Y338PQX1bmfy08i9QQCB7/rcUAVntfw==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.0.3.tgz", + "integrity": "sha512-L/4pUDMxcNa8R/EthV08Zt42WBO4h1rarVtK0K+QJG0X187OLo7l699jWw0GKuwzkPQ//jMFA/8Xm6Fh3J/DAg==", "dev": true }, "pump": { @@ -3938,10 +4022,10 @@ "dev": true }, "typescript": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.0.4.tgz", - "integrity": "sha512-cW9T5W9xY37cc+jfEnaUvX91foxtHkza3Nw3wkoF4sSlKn0MONdkdEndig/qPBWXNkmplh3NzayQzCiHM4/hqw==", - "dev": true + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz", + "integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==", + "devOptional": true }, "unfetch": { "version": "5.0.0", @@ -3957,9 +4041,9 @@ } }, "uuid": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz", - "integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==" + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==" }, "web-streams-polyfill": { "version": "3.2.1", @@ -3980,9 +4064,9 @@ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" }, "ws": { - "version": "8.13.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.13.0.tgz", - "integrity": "sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==", + "version": "8.14.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.14.1.tgz", + "integrity": "sha512-4OOseMUq8AzRBI/7SLMUwO+FEDnguetSk7KMb1sHwvF2w2Wv5Hoj0nlifx8vtGsftE/jWHojPy8sMMzYLJ2G/A==", "requires": {} }, "yallist": { @@ -3992,9 +4076,9 @@ "dev": true }, "yaml": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.1.tgz", - "integrity": "sha512-2eHWfjaoXgTBC2jNM1LRef62VQa0umtvRiDSk6HSzW7RvS5YtkabJrwYLLEKWBc8a5U2PTSCs+dJjUTJdlHsWQ==" + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.2.tgz", + "integrity": "sha512-N/lyzTPaJasoDmfV7YTrYCI0G/3ivm/9wdG0aHuheKowWQwGTsK0Eoiw6utmzAnI6pkJa0DUVygvp3spqqEKXg==" }, "yauzl": { "version": "2.10.0", diff --git a/e2e-tests/playwright/package.json b/e2e-tests/playwright/package.json index 6b491cf198b..5f9aac389bf 100644 --- a/e2e-tests/playwright/package.json +++ b/e2e-tests/playwright/package.json @@ -4,34 +4,36 @@ "percy": "cross-env PERCY_TOKEN=$PERCY_TOKEN PW_PERCY_ENABLE=true percy exec -- playwright test --project=chrome --project=iphone --project=ipad", "tsc": "tsc -b", "lint": "eslint . --ext .js,.ts", - "prettier": "prettier --write .", + "prettier": "prettier . --check", + "prettier:fix": "prettier --write .", "check": "npm run tsc && npm run lint && npm run prettier", "codegen": "cross-env playwright codegen $PW_BASE_URL", "playwright-ui": "playwright test --ui", "test-slomo": "cross-env PW_SNAPSHOT_ENABLE=true PW_SLOWMO=1000 playwright test", - "show-report": "npx playwright show-report" + "show-report": "npx playwright show-report", + "postinstall": "npx playwright install" }, "dependencies": { "@axe-core/playwright": "4.7.3", - "@percy/cli": "1.26.2", + "@percy/cli": "1.27.1", "@percy/playwright": "1.0.4", - "@playwright/test": "1.36.1", + "@playwright/test": "1.38.0", "async-wait-until": "2.0.12", - "axe-core": "4.7.2", + "axe-core": "4.8.1", "chalk": "4.1.2", "deepmerge": "4.3.1", "dotenv": "16.3.1", "form-data": "4.0.0", "isomorphic-unfetch": "4.0.2", - "uuid": "9.0.0" + "uuid": "9.0.1" }, "devDependencies": { - "@types/uuid": "9.0.2", - "@typescript-eslint/eslint-plugin": "6.1.0", - "@typescript-eslint/parser": "6.1.0", + "@types/uuid": "9.0.4", + "@typescript-eslint/eslint-plugin": "6.7.0", + "@typescript-eslint/parser": "6.7.0", "cross-env": "7.0.3", - "eslint": "8.45.0", - "prettier": "2.8.7", - "typescript": "5.0.4" + "eslint": "8.49.0", + "prettier": "3.0.3", + "typescript": "5.2.2" } } diff --git a/e2e-tests/playwright/playwright.config.ts b/e2e-tests/playwright/playwright.config.ts index 56fcbd2c30c..7b687cabea1 100644 --- a/e2e-tests/playwright/playwright.config.ts +++ b/e2e-tests/playwright/playwright.config.ts @@ -52,13 +52,6 @@ export default defineConfig({ }, }, projects: [ - { - name: 'iphone', - use: { - browserName: 'chromium', - ...devices['iPhone 13 Pro'], - }, - }, { name: 'ipad', use: { diff --git a/e2e-tests/playwright/sample.env b/e2e-tests/playwright/sample.env index 73ffe199810..a69c4c8fe7e 100644 --- a/e2e-tests/playwright/sample.env +++ b/e2e-tests/playwright/sample.env @@ -16,41 +16,38 @@ # 5. PW_ADMIN_EMAIL # - Default to "sysadmin@sample.mattermost.com" if not set. -# 6. PW_BOARDS_PRODUCT_ENABLED -# - Default to "true" if not set. Used to correctly set server config. - -# 7. PW_HA_CLUSTER_ENABLED +# 6. PW_HA_CLUSTER_ENABLED # - Default to "false" if not set. Set to true if the test server is with HA enabled. -# 8. PW_HA_CLUSTER_NODE_COUNT +# 7. PW_HA_CLUSTER_NODE_COUNT # - Default to "2" if not set. -# 9. PW_HA_CLUSTER_NAME +# 8. PW_HA_CLUSTER_NAME # - Default to "mm_dev_cluster" if not set. -# 10. PW_RESET_BEFORE_TEST +# 9. PW_RESET_BEFORE_TEST # - Default to "false" if not set. If true, the setup deletes all teams and channels other than the default team which is "ad-1". -# 11. CI +# 10. CI # - Default to "false" if not set. -# 12. PW_HEADLESS +# 11. PW_HEADLESS # - Default to "false" or headless mode if not set. Set to true to run test in headed mode. -# 13. PW_SLOWMO +# 12. PW_SLOWMO # - Default to "0" if not set which means normal test speed run. Slows down Playwright operations by the specified amount of milliseconds. Useful so that you can see what is going on. -# 14. PW_WORKERS +# 13. PW_WORKERS # - Default to "1" if not set. The maximum number of concurrent worker processes to use for parallelizing tests. -# 15. PW_SNAPSHOT_ENABLE +# 14. PW_SNAPSHOT_ENABLE # - Default to "false" if not set. Set to true to enable snapshot testing. # Note that, snapshot testing should be done in Playwright docker image only. # This is to ensure that, there's a common base platform for all contributors # regardless of each local development platform. -# 16. PW_PERCY_ENABLE +# 15. PW_PERCY_ENABLE # - Default to "false" if not set. Use to save and compare results via https://percy.io/. -# 17. PERCY_TOKEN +# 16. PERCY_TOKEN # - A token required by https://percy.io/. diff --git a/e2e-tests/playwright/support/constant.ts b/e2e-tests/playwright/support/constant.ts index 8be0ec803d4..01dd425610a 100644 --- a/e2e-tests/playwright/support/constant.ts +++ b/e2e-tests/playwright/support/constant.ts @@ -2,6 +2,5 @@ // See LICENSE.txt for license information. export const appsPluginId = 'com.mattermost.apps'; -export const boardsPluginId = 'focalboard'; export const callsPluginId = 'com.mattermost.calls'; export const playbooksPluginId = 'playbooks'; diff --git a/e2e-tests/playwright/support/flag.ts b/e2e-tests/playwright/support/flag.ts index 8e9a451c732..d346e53f1d2 100644 --- a/e2e-tests/playwright/support/flag.ts +++ b/e2e-tests/playwright/support/flag.ts @@ -3,11 +3,10 @@ import os from 'node:os'; -import {expect, test} from '@playwright/test'; +import {expect} from '@playwright/test'; import {callsPluginId} from './constant'; import {getAdminClient} from './server/init'; -import {isSmallScreen} from './util'; export async function shouldHaveCallsEnabled(enabled = true) { const {adminClient} = await getAdminClient(); @@ -26,14 +25,10 @@ export async function shouldHaveFeatureFlag(name: string, value: string | boolea const matched = config.FeatureFlags[name] === value; expect( matched, - matched ? '' : `FeatureFlags["${name}'] expect "${value}" but actual "${config.FeatureFlags[name]}"` + matched ? '' : `FeatureFlags["${name}'] expect "${value}" but actual "${config.FeatureFlags[name]}"`, ).toBeTruthy(); } -export function shouldSkipInSmallScreen() { - test.skip(({viewport}) => isSmallScreen(viewport), 'Not applicable to mobile device'); -} - export async function shouldRunInLinux() { const platform = os.platform(); await expect(platform, 'Run in Linux or Playwright docker image only').toBe('linux'); diff --git a/e2e-tests/playwright/support/server/default_config.ts b/e2e-tests/playwright/support/server/default_config.ts index c57222c8a74..97addf26490 100644 --- a/e2e-tests/playwright/support/server/default_config.ts +++ b/e2e-tests/playwright/support/server/default_config.ts @@ -35,9 +35,6 @@ const onPremServerConfig = (): Partial => { Enable: testConfig.haClusterEnabled, ClusterName: testConfig.haClusterName, }, - ExperimentalSettings: { - DisableAppBar: false, - }, PasswordSettings: { MinimumLength: 5, Lowercase: false, @@ -48,9 +45,15 @@ const onPremServerConfig = (): Partial => { }, PluginSettings: { EnableUploads: true, - Plugins: { + PluginStates: { 'com.mattermost.calls': { - defaultenabled: true, + Enable: false, + }, + 'com.mattermost.nps': { + Enable: false, + }, + playbooks: { + Enable: true, }, }, }, @@ -65,7 +68,7 @@ const onPremServerConfig = (): Partial => { }; // Should be based only from the generated default config from ./server via "make config-reset" -// Based on v7.10 server +// Based on v9.1 server const defaultServerConfig: AdminConfig = { ServiceSettings: { SiteURL: '', @@ -127,8 +130,7 @@ const defaultServerConfig: AdminConfig = { WebsocketPort: 80, WebserverMode: 'gzip', EnableGifPicker: true, - GfycatAPIKey: '2_KtH_W5', - GfycatAPISecret: '3wLVZPiswc3DnaiaFoLkDvB4X0IV6CpMkj4tf2inJRsBY6-FnkT08zGmppWFgeof', + GiphySdkKey: 's0glxvzVg9azvPipKxcPLpXV0q1x1fVP', EnableCustomEmoji: true, EnableEmojiPicker: true, PostEditTimeLimit: -1, @@ -158,6 +160,11 @@ const defaultServerConfig: AdminConfig = { EnableLatex: false, EnableInlineLatex: true, PostPriority: true, + AllowPersistentNotifications: true, + AllowPersistentNotificationsForGuests: false, + PersistentNotificationIntervalMinutes: 5, + PersistentNotificationMaxCount: 6, + PersistentNotificationMaxRecipients: 5, EnableAPIChannelDeletion: false, EnableLocalMode: false, LocalModeSocketLocation: '/var/tmp/mattermost_local.socket', @@ -171,15 +178,11 @@ const defaultServerConfig: AdminConfig = { EnableCustomGroups: true, SelfHostedPurchase: true, AllowSyncedDrafts: true, - AllowPersistentNotifications: true, - PersistentNotificationMaxCount: 6, - PersistentNotificationMaxRecipients: 5, - PersistentNotificationIntervalMinutes: 5, - AllowPersistentNotificationsForGuests: false, }, TeamSettings: { SiteName: 'Mattermost', MaxUsersPerTeam: 50, + EnableJoinLeaveMessageByDefault: true, EnableUserCreation: true, EnableOpenServer: false, EnableUserDeactivation: false, @@ -223,6 +226,7 @@ const defaultServerConfig: AdminConfig = { DisableDatabaseSearch: false, MigrationsStatementTimeoutSeconds: 100000, ReplicaLagSettings: [], + ReplicaMonitorIntervalSeconds: 5, }, LogSettings: { EnableConsole: true, @@ -237,6 +241,7 @@ const defaultServerConfig: AdminConfig = { EnableDiagnostics: true, VerboseDiagnostics: false, EnableSentry: true, + AdvancedLoggingJSON: {}, AdvancedLoggingConfig: '', }, ExperimentalAuditSettings: { @@ -247,6 +252,7 @@ const defaultServerConfig: AdminConfig = { FileMaxBackups: 0, FileCompress: false, FileMaxQueueSize: 1000, + AdvancedLoggingJSON: {}, AdvancedLoggingConfig: '', }, NotificationLogSettings: { @@ -258,6 +264,7 @@ const defaultServerConfig: AdminConfig = { FileLevel: 'INFO', FileJson: true, FileLocation: '', + AdvancedLoggingJSON: {}, AdvancedLoggingConfig: '', }, PasswordSettings: { @@ -293,6 +300,21 @@ const defaultServerConfig: AdminConfig = { AmazonS3SSE: false, AmazonS3Trace: false, AmazonS3RequestTimeoutMilliseconds: 30000, + DedicatedExportStore: false, + ExportDriverName: 'local', + ExportDirectory: './data/', + ExportAmazonS3AccessKeyId: '', + ExportAmazonS3SecretAccessKey: '', + ExportAmazonS3Bucket: '', + ExportAmazonS3PathPrefix: '', + ExportAmazonS3Region: '', + ExportAmazonS3Endpoint: 's3.amazonaws.com', + ExportAmazonS3SSL: true, + ExportAmazonS3SignV2: false, + ExportAmazonS3SSE: false, + ExportAmazonS3Trace: false, + ExportAmazonS3RequestTimeoutMilliseconds: 30000, + ExportAmazonS3PresignExpiresSeconds: 21600, }, EmailSettings: { EnableSignUpWithEmail: true, @@ -344,7 +366,7 @@ const defaultServerConfig: AdminConfig = { SupportSettings: { TermsOfServiceLink: 'https://mattermost.com/pl/terms-of-use/', PrivacyPolicyLink: 'https://mattermost.com/pl/privacy-policy/', - AboutLink: 'https://docs.mattermost.com/pl/about-mattermost', + AboutLink: 'https://mattermost.com/pl/about-mattermost', HelpLink: 'https://mattermost.com/pl/help/', ReportAProblemLink: 'https://mattermost.com/pl/report-a-bug', ForgotPasswordLink: '', @@ -539,7 +561,7 @@ const defaultServerConfig: AdminConfig = { UseNewSAMLLibrary: false, EnableSharedChannels: false, EnableRemoteClusterService: false, - DisableAppBar: true, + DisableAppBar: false, DisableRefetchingOnBrowserFocus: false, DelayChannelAutocomplete: false, }, @@ -589,6 +611,8 @@ const defaultServerConfig: AdminConfig = { BoardsRetentionDays: 365, DeletionJobStartTime: '02:00', BatchSize: 3000, + TimeBetweenBatchesMilliseconds: 100, + RetentionIdsBatchSize: 100, }, MessageExportSettings: { EnableExport: false, @@ -627,6 +651,9 @@ const defaultServerConfig: AdminConfig = { 'com.mattermost.nps': { Enable: true, }, + playbooks: { + Enable: true, + }, }, EnableMarketplace: true, EnableRemoteMarketplace: true, @@ -638,6 +665,7 @@ const defaultServerConfig: AdminConfig = { }, DisplaySettings: { CustomURLSchemes: [], + MaxMarkdownNodes: 0, ExperimentalTimezone: true, }, GuestAccountsSettings: { @@ -656,32 +684,24 @@ const defaultServerConfig: AdminConfig = { CloudSettings: { CWSURL: 'https://customers.mattermost.com', CWSAPIURL: 'https://portal.internal.prod.cloud.mattermost.com', + CWSMock: false, }, FeatureFlags: { TestFeature: 'off', TestBoolFeature: false, EnableRemoteClusterService: false, AppsEnabled: true, - PluginPlaybooks: '', - PluginApps: '', - PluginFocalboard: '', - PluginCalls: '', - PermalinkPreviews: true, + PermalinkPreviews: false, CallsEnabled: true, - BoardsFeatureFlags: '', - BoardsDataRetention: false, NormalizeLdapDNs: false, GraphQL: false, - CommandPalette: false, - SendWelcomePost: true, - PostPriority: true, + PostPriority: false, WysiwygEditor: false, - PeopleProduct: false, - ReduceOnBoardingTaskList: false, - ThreadsEverywhere: false, OnboardingTourTips: true, DeprecateCloudFree: false, CloudReverseTrial: false, + EnableExportDirectDownload: false, + StreamlinedMarketplace: true, }, ImportSettings: { Directory: './import', diff --git a/e2e-tests/playwright/support/server/init.ts b/e2e-tests/playwright/support/server/init.ts index f5d8f099859..d0a74a6b119 100644 --- a/e2e-tests/playwright/support/server/init.ts +++ b/e2e-tests/playwright/support/server/init.ts @@ -24,7 +24,7 @@ export async function initSetup({ const {adminClient, adminUser} = await getAdminClient(); if (!adminClient) { throw new Error( - "Failed to setup admin: Check that you're able to access the server using the same admin credential." + "Failed to setup admin: Check that you're able to access the server using the same admin credential.", ); } @@ -83,8 +83,8 @@ export async function initSetup({ // eslint-disable-next-line no-console console.log( chalk.green( - `This failed due to the experimental fetch support in Node.js starting v18.0.0.\nYou may set environment variable: "export NODE_OPTIONS='--no-experimental-fetch'", then try again.'` - ) + `This failed due to the experimental fetch support in Node.js starting v18.0.0.\nYou may set environment variable: "export NODE_OPTIONS='--no-experimental-fetch'", then try again.'`, + ), ); } expect(err, 'Should not throw an error').toBeFalsy(); diff --git a/e2e-tests/playwright/support/test_action.ts b/e2e-tests/playwright/support/test_action.ts index 8cb9428c4eb..95a0aada572 100644 --- a/e2e-tests/playwright/support/test_action.ts +++ b/e2e-tests/playwright/support/test_action.ts @@ -16,6 +16,6 @@ export async function hideDynamicChannelsContent(page: Page) { export async function waitForAnimationEnd(locator: Locator) { return locator.evaluate((element) => - Promise.all(element.getAnimations({subtree: true}).map((animation) => animation.finished)) + Promise.all(element.getAnimations({subtree: true}).map((animation) => animation.finished)), ); } diff --git a/e2e-tests/playwright/support/test_fixture.ts b/e2e-tests/playwright/support/test_fixture.ts index f43ba0c8255..be63bb72525 100644 --- a/e2e-tests/playwright/support/test_fixture.ts +++ b/e2e-tests/playwright/support/test_fixture.ts @@ -1,11 +1,10 @@ -import {test as base, Browser, Page, ViewportSize} from '@playwright/test'; +import {test as base, Browser, Page} from '@playwright/test'; import {AxeResults} from 'axe-core'; import AxeBuilder from '@axe-core/playwright'; import {TestBrowser} from './browser_context'; -import {shouldHaveCallsEnabled, shouldHaveFeatureFlag, shouldSkipInSmallScreen, shouldRunInLinux} from './flag'; +import {shouldHaveCallsEnabled, shouldHaveFeatureFlag, shouldRunInLinux} from './flag'; import {initSetup, getAdminClient} from './server'; -import {isSmallScreen} from './util'; import {hideDynamicChannelsContent, waitForAnimationEnd, waitUntil} from './test_action'; import {pages} from './ui/pages'; import {matchSnapshot} from './visual'; @@ -29,8 +28,8 @@ export const test = base.extend({ const ab = new AxeBuilderExtended(); await use(ab); }, - pw: async ({browser, viewport}, use) => { - const pw = new PlaywrightExtended(browser, viewport); + pw: async ({browser}, use) => { + const pw = new PlaywrightExtended(browser); await use(pw); await pw.testBrowser.close(); }, @@ -47,7 +46,6 @@ class PlaywrightExtended { // ./flag readonly shouldHaveCallsEnabled; readonly shouldHaveFeatureFlag; - readonly shouldSkipInSmallScreen; readonly shouldRunInLinux; // ./server @@ -62,20 +60,16 @@ class PlaywrightExtended { // ./ui/pages readonly pages; - // ./util - readonly isSmallScreen; - // ./visual readonly matchSnapshot; - constructor(browser: Browser, viewport: ViewportSize | null) { + constructor(browser: Browser) { // ./browser_context this.testBrowser = new TestBrowser(browser); // ./flag this.shouldHaveCallsEnabled = shouldHaveCallsEnabled; this.shouldHaveFeatureFlag = shouldHaveFeatureFlag; - this.shouldSkipInSmallScreen = shouldSkipInSmallScreen; this.shouldRunInLinux = shouldRunInLinux; // ./server @@ -90,9 +84,6 @@ class PlaywrightExtended { // ./ui/pages this.pages = pages; - // ./util - this.isSmallScreen = () => isSmallScreen(viewport); - // ./visual this.matchSnapshot = matchSnapshot; } diff --git a/e2e-tests/playwright/support/ui/components/boards/create_modal.ts b/e2e-tests/playwright/support/ui/components/boards/create_modal.ts deleted file mode 100644 index ed3f21a2a37..00000000000 --- a/e2e-tests/playwright/support/ui/components/boards/create_modal.ts +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import {expect, Locator} from '@playwright/test'; - -export default class BoardsCreateModal { - readonly container: Locator; - - readonly productSwitchMenu; - - constructor(container: Locator) { - this.container = container; - - this.productSwitchMenu = container.getByRole('button', {name: 'Product switch menu'}); - } - - async switchProduct(name: string) { - await this.productSwitchMenu.click(); - await this.container.getByRole('link', {name: ` ${name}`}).click(); - } - - async toBeVisible(name: string) { - await expect(this.container.getByRole('heading', {name})).toBeVisible(); - } -} - -export {BoardsCreateModal}; diff --git a/e2e-tests/playwright/support/ui/components/boards/sidebar.ts b/e2e-tests/playwright/support/ui/components/boards/sidebar.ts deleted file mode 100644 index 372054524c0..00000000000 --- a/e2e-tests/playwright/support/ui/components/boards/sidebar.ts +++ /dev/null @@ -1,28 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import {Locator} from '@playwright/test'; - -export default class BoardsSidebar { - readonly container: Locator; - - readonly plusButton; - readonly createNewBoardMenuItem; - readonly createNewCategoryMenuItem; - readonly titles; - - constructor(container: Locator) { - this.container = container; - - this.plusButton = container.locator('.add-board-icon'); - this.createNewBoardMenuItem = container.getByRole('button', {name: 'Create new board'}); - this.createNewCategoryMenuItem = container.getByRole('button', {name: 'Create New Category'}); - this.titles = container.locator('.SidebarBoardItem > .octo-sidebar-title'); - } - - async waitForTitle(name: string) { - await this.container.getByRole('button', {name: ` ${name}`}).waitFor({state: 'visible'}); - } -} - -export {BoardsSidebar}; diff --git a/e2e-tests/playwright/support/ui/components/channels/center_view.ts b/e2e-tests/playwright/support/ui/components/channels/center_view.ts new file mode 100644 index 00000000000..ffd91d0ac04 --- /dev/null +++ b/e2e-tests/playwright/support/ui/components/channels/center_view.ts @@ -0,0 +1,93 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {expect, Locator} from '@playwright/test'; + +import {components} from '@e2e-support/ui/components'; +import {waitUntil} from '@e2e-support/test_action'; +import {duration} from '@e2e-support/util'; + +export default class ChannelsCenterView { + readonly container: Locator; + + readonly header; + readonly headerMobile; + readonly postCreate; + + constructor(container: Locator) { + this.container = container; + + this.header = new components.ChannelsHeader(this.container.locator('.channel-header')); + this.headerMobile = new components.ChannelsHeaderMobile(this.container.locator('.navbar')); + this.postCreate = new components.ChannelsPostCreate(container.getByTestId('post-create')); + } + + async toBeVisible() { + await expect(this.container).toBeVisible(); + await this.postCreate.toBeVisible(); + } + + /** + * Return the first post in the Center + */ + async getFirstPost() { + const firstPost = this.container.getByTestId('postView').first(); + await firstPost.waitFor(); + return new components.ChannelsPost(firstPost); + } + + /** + * Return the last post in the Center + */ + async getLastPost() { + const lastPost = this.container.getByTestId('postView').last(); + await lastPost.waitFor(); + return new components.ChannelsPost(lastPost); + } + + /** + * Return the Nth post in the Center from the top + * @param index + * @returns + */ + async getNthPost(index: number) { + const nthPost = this.container.getByTestId('postView').nth(index); + await nthPost.waitFor(); + return new components.ChannelsPost(nthPost); + } + + /** + * Returns the Center post by post's id + * @param postId Just the ID without the prefix + */ + async getPostById(id: string) { + const postById = this.container.locator(`[id="post_${id}"]`); + await postById.waitFor(); + return new components.ChannelsPost(postById); + } + + async waitUntilLastPostContains(text: string, timeout = duration.ten_sec) { + await waitUntil( + async () => { + const post = await this.getLastPost(); + const content = await post.container.textContent(); + return content?.includes(text); + }, + {timeout}, + ); + } + + async waitUntilPostWithIdContains(id: string, text: string, timeout = duration.ten_sec) { + await waitUntil( + async () => { + const post = await this.getPostById(id); + const content = await post.container.textContent(); + + return content?.includes(text); + }, + {timeout}, + ); + } +} + +export {ChannelsCenterView}; diff --git a/e2e-tests/playwright/support/ui/components/channels/emoji_gif_picker.ts b/e2e-tests/playwright/support/ui/components/channels/emoji_gif_picker.ts new file mode 100644 index 00000000000..08faa656832 --- /dev/null +++ b/e2e-tests/playwright/support/ui/components/channels/emoji_gif_picker.ts @@ -0,0 +1,61 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {expect, Locator} from '@playwright/test'; + +export default class EmojiGifPicker { + readonly container: Locator; + + readonly gifTab: Locator; + readonly gifSearchInput: Locator; + readonly gifPickerItems: Locator; + + constructor(container: Locator) { + this.container = container; + + this.gifTab = container.getByText('GIFs'); + this.gifSearchInput = container.getByPlaceholder('Search GIPHY'); + this.gifPickerItems = container.locator('.gif-picker__items'); + } + + async toBeVisible() { + await expect(this.container).toBeVisible(); + } + + async openGifTab() { + await expect(this.gifTab).toBeVisible(); + + await this.gifTab.click({force: true}); + + await expect(this.gifSearchInput).toBeVisible(); + await expect(this.gifPickerItems).toBeVisible(); + } + + async searchGif(name: string) { + await this.gifSearchInput.fill(name); + await expect(this.gifSearchInput).toHaveValue(name); + } + + async getNthGif(n: number) { + await expect(this.gifPickerItems).toBeVisible(); + + await this.gifPickerItems.locator('img').nth(n).waitFor(); + const nthGif = this.gifPickerItems.locator('img').nth(n); + await expect(nthGif).toBeVisible(); + + const nthGifSrc = await nthGif.getAttribute('src'); + const nthGifAlt = await nthGif.getAttribute('alt'); + + if (!nthGifSrc || !nthGifAlt) { + throw new Error('Gif src or alt is empty'); + } + + return { + src: nthGifSrc, + alt: nthGifAlt, + img: nthGif, + }; + } +} + +export {EmojiGifPicker}; diff --git a/e2e-tests/playwright/support/ui/components/channels/header_mobile.ts b/e2e-tests/playwright/support/ui/components/channels/header_mobile.ts index a687220abf4..50b39e6218a 100644 --- a/e2e-tests/playwright/support/ui/components/channels/header_mobile.ts +++ b/e2e-tests/playwright/support/ui/components/channels/header_mobile.ts @@ -10,13 +10,13 @@ export default class ChannelsHeaderMobile { this.container = container; } - async toggleSidebar() { - await this.container.getByRole('button', {name: 'Toggle sidebar Menu Icon'}).click(); - } - async toBeVisible() { await expect(this.container).toBeVisible(); } + + async toggleSidebar() { + await this.container.getByRole('button', {name: 'Toggle sidebar Menu Icon'}).click(); + } } export {ChannelsHeaderMobile}; diff --git a/e2e-tests/playwright/support/ui/components/channels/post_create.ts b/e2e-tests/playwright/support/ui/components/channels/post_create.ts index b43a213e918..14463e3028a 100644 --- a/e2e-tests/playwright/support/ui/components/channels/post_create.ts +++ b/e2e-tests/playwright/support/ui/components/channels/post_create.ts @@ -7,30 +7,76 @@ export default class ChannelsPostCreate { readonly container: Locator; readonly input; + readonly attachmentButton; readonly emojiButton; readonly sendMessageButton; - constructor(container: Locator) { + constructor(container: Locator, isRHS = false) { this.container = container; - this.input = container.getByTestId('post_textbox'); + if (!isRHS) { + this.input = container.getByTestId('post_textbox'); + } else { + this.input = container.getByTestId('reply_textbox'); + } + this.attachmentButton = container.getByLabel('attachment'); this.emojiButton = container.getByLabel('select an emoji'); this.sendMessageButton = container.getByTestId('SendMessageButton'); } - async postMessage(message: string) { + async toBeVisible() { + await expect(this.container).toBeVisible(); + + await this.input.waitFor(); + await expect(this.input).toBeVisible(); + } + + /** + * It just writes the message in the input and doesn't send it + * @param message : Message to be written in the input + */ + async writeMessage(message: string) { + await this.input.waitFor(); + await expect(this.input).toBeVisible(); + await this.input.fill(message); } + /** + * Returns the value of the message input + */ + async getInputValue() { + await expect(this.input).toBeVisible(); + return await this.input.inputValue(); + } + + /** + * Sends the message already written in the input + */ async sendMessage() { + await expect(this.input).toBeVisible(); + const messageInputValue = await this.getInputValue(); + expect(messageInputValue).not.toBe(''); + + await expect(this.sendMessageButton).toBeVisible(); + await expect(this.sendMessageButton).toBeEnabled(); + await this.sendMessageButton.click(); } - async toBeVisible() { - await expect(this.container).toBeVisible(); - await expect(this.input).toBeVisible(); + /** + * Composes and sends a message + */ + async postMessage(message: string) { + await this.writeMessage(message); + await this.sendMessage(); + } + + async openEmojiPicker() { + await expect(this.emojiButton).toBeVisible(); + await this.emojiButton.click(); } } diff --git a/e2e-tests/playwright/support/ui/components/channels/post_dot_menu.ts b/e2e-tests/playwright/support/ui/components/channels/post_dot_menu.ts index 600aaef6364..ca953247770 100644 --- a/e2e-tests/playwright/support/ui/components/channels/post_dot_menu.ts +++ b/e2e-tests/playwright/support/ui/components/channels/post_dot_menu.ts @@ -6,22 +6,43 @@ import {expect, Locator} from '@playwright/test'; export default class PostDotMenu { readonly container: Locator; + readonly replyMenuItem; + readonly forwardMenuItem; + readonly followMessageMenuItem; + readonly markAsUnreadMenuItem; + readonly remindMenuItem; + readonly saveMenuItem; + readonly removeFromSavedMenuItem; + readonly pinToChannelMenuItem; + readonly unpinFromChannelMenuItem; + readonly copyLinkMenuItem; + readonly editMenuItem; + readonly copyTextMenuItem; readonly deleteMenuItem; constructor(container: Locator) { this.container = container; - this.deleteMenuItem = this.container.getByText('Delete', {exact: true}); + const getMenuItem = (hasText: string) => container.getByRole('menuitem').filter({hasText}); + + this.replyMenuItem = getMenuItem('Reply'); + this.forwardMenuItem = getMenuItem('Forward'); + this.followMessageMenuItem = getMenuItem('Follow message'); + this.markAsUnreadMenuItem = getMenuItem('Mark as Unread'); + this.remindMenuItem = getMenuItem('Remind'); + this.saveMenuItem = getMenuItem('Save'); + this.removeFromSavedMenuItem = getMenuItem('Remove from Saved'); + this.pinToChannelMenuItem = getMenuItem('Pin to Channel'); + this.unpinFromChannelMenuItem = getMenuItem('Unpin from Channel'); + this.copyLinkMenuItem = getMenuItem('Copy Link'); + this.editMenuItem = getMenuItem('Edit'); + this.copyTextMenuItem = getMenuItem('Copy Text'); + this.deleteMenuItem = getMenuItem('Delete'); } async toBeVisible() { await expect(this.container).toBeVisible(); } - - async delete() { - await this.deleteMenuItem.waitFor(); - await this.deleteMenuItem.click(); - } } export {PostDotMenu}; diff --git a/e2e-tests/playwright/support/ui/components/channels/post_menu.ts b/e2e-tests/playwright/support/ui/components/channels/post_menu.ts index ba97b07b667..6e28058308a 100644 --- a/e2e-tests/playwright/support/ui/components/channels/post_menu.ts +++ b/e2e-tests/playwright/support/ui/components/channels/post_menu.ts @@ -6,12 +6,24 @@ import {expect, Locator} from '@playwright/test'; export default class PostMenu { readonly container: Locator; + readonly plusOneEmojiButton; + readonly grinningEmojiButton; + readonly whiteCheckMarkEmojiButton; + readonly addReactionButton; + readonly saveButton; readonly replyButton; + readonly actionsButton; readonly dotMenuButton; constructor(container: Locator) { this.container = container; + this.plusOneEmojiButton = container.getByRole('button', {name: '+1 emoji'}); + this.grinningEmojiButton = container.getByRole('button', {name: 'grinning emoji'}); + this.whiteCheckMarkEmojiButton = container.getByRole('button', {name: 'white check mark emoji'}); + this.addReactionButton = container.getByRole('button', {name: 'add reaction'}); + this.saveButton = container.getByRole('button', {name: 'save'}); + this.actionsButton = container.getByRole('button', {name: 'actions'}); this.replyButton = container.getByRole('button', {name: 'reply'}); this.dotMenuButton = container.getByRole('button', {name: 'more'}); } diff --git a/e2e-tests/playwright/support/ui/components/channels/post_reminder_menu.ts b/e2e-tests/playwright/support/ui/components/channels/post_reminder_menu.ts new file mode 100644 index 00000000000..08e3e53f330 --- /dev/null +++ b/e2e-tests/playwright/support/ui/components/channels/post_reminder_menu.ts @@ -0,0 +1,32 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {expect, Locator} from '@playwright/test'; + +export default class PostReminderMenu { + readonly container: Locator; + + readonly thirtyMinsMenuItem; + readonly oneHourMenuItem; + readonly twoHoursMenuItem; + readonly tomorrowMenuItem; + readonly customMenuItem; + + constructor(container: Locator) { + this.container = container; + + const getMenuItem = (hasText: string) => container.getByRole('menuitem').filter({hasText}); + + this.thirtyMinsMenuItem = getMenuItem('30 mins'); + this.oneHourMenuItem = getMenuItem('1 hour'); + this.twoHoursMenuItem = getMenuItem('2 hours'); + this.tomorrowMenuItem = getMenuItem('Tomorrow'); + this.customMenuItem = getMenuItem('Custom'); + } + + async toBeVisible() { + await expect(this.container).toBeVisible(); + } +} + +export {PostReminderMenu}; diff --git a/e2e-tests/playwright/support/ui/components/channels/sidebar_right.ts b/e2e-tests/playwright/support/ui/components/channels/sidebar_right.ts index 852d94d6e11..f5a66d11165 100644 --- a/e2e-tests/playwright/support/ui/components/channels/sidebar_right.ts +++ b/e2e-tests/playwright/support/ui/components/channels/sidebar_right.ts @@ -8,16 +8,13 @@ import {components} from '@e2e-support/ui/components'; export default class ChannelsSidebarRight { readonly container: Locator; - readonly input; - readonly sendMessageButton; + readonly postCreate; readonly closeButton; constructor(container: Locator) { this.container = container; - this.input = container.getByTestId('reply_textbox'); - this.sendMessageButton = container.getByTestId('SendMessageButton'); - + this.postCreate = new components.ChannelsPostCreate(container.getByTestId('comment-create'), true); this.closeButton = container.locator('#rhsCloseButton'); } @@ -25,29 +22,23 @@ export default class ChannelsSidebarRight { await expect(this.container).toBeVisible(); } - async postMessage(message: string) { - await this.input.fill(message); - } - - async sendMessage() { - await this.sendMessageButton.click(); - } - - /** - * Returns the value of the textbox in RHS - */ - async getInputValue() { - return await this.input.inputValue(); - } - /** * Returns the RHS post by post id * @param postId Just the ID without the prefix */ - async getRHSPostById(postId: string) { - const rhsPostId = `rhsPost_${postId}`; - const postLocator = this.container.locator(`#${rhsPostId}`); - return new components.ChannelsPost(postLocator); + async getPostById(postId: string) { + const post = this.container.locator(`[id="rhsPost_${postId}"]`); + await post.waitFor(); + return new components.ChannelsPost(post); + } + + /** + * Return the last post in the RHS + */ + async getLastPost() { + const post = this.container.getByTestId('rhsPostView').last(); + await post.waitFor(); + return new components.ChannelsPost(post); } /** diff --git a/e2e-tests/playwright/support/ui/components/index.ts b/e2e-tests/playwright/support/ui/components/index.ts index 806c21aaec2..ca2074c1085 100644 --- a/e2e-tests/playwright/support/ui/components/index.ts +++ b/e2e-tests/playwright/support/ui/components/index.ts @@ -1,56 +1,60 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import {BoardsSidebar} from './boards/sidebar'; import {ChannelsHeader} from './channels/header'; import {ChannelsHeaderMobile} from './channels/header_mobile'; import {ChannelsAppBar} from './channels/app_bar'; import {ChannelsPostCreate} from './channels/post_create'; import {ChannelsPost} from './channels/post'; +import {ChannelsCenterView} from './channels/center_view'; import {ChannelsSidebarLeft} from './channels/sidebar_left'; import {ChannelsSidebarRight} from './channels/sidebar_right'; +import {DeletePostModal} from './channels/delete_post_modal'; import {FindChannelsModal} from './channels/find_channels_modal'; import {Footer} from './footer'; import {GlobalHeader} from './global_header'; import {MainHeader} from './main_header'; import {PostDotMenu} from './channels/post_dot_menu'; -import {DeletePostModal} from './channels/delete_post_modal'; +import {PostReminderMenu} from './channels/post_reminder_menu'; import {PostMenu} from './channels/post_menu'; import {ThreadFooter} from './channels/thread_footer'; +import {EmojiGifPicker} from './channels/emoji_gif_picker'; const components = { - BoardsSidebar, + GlobalHeader, + ChannelsCenterView, + ChannelsSidebarLeft, + ChannelsSidebarRight, ChannelsAppBar, ChannelsHeader, ChannelsHeaderMobile, ChannelsPostCreate, ChannelsPost, - ChannelsSidebarLeft, - ChannelsSidebarRight, FindChannelsModal, - Footer, - GlobalHeader, - MainHeader, - PostDotMenu, DeletePostModal, + PostDotMenu, PostMenu, ThreadFooter, + Footer, + MainHeader, + PostReminderMenu, + EmojiGifPicker, }; export { components, - BoardsSidebar, + GlobalHeader, + ChannelsCenterView, + ChannelsSidebarLeft, + ChannelsSidebarRight, ChannelsAppBar, ChannelsHeader, ChannelsHeaderMobile, ChannelsPostCreate, ChannelsPost, - ChannelsSidebarLeft, - ChannelsSidebarRight, FindChannelsModal, - GlobalHeader, - PostDotMenu, DeletePostModal, + PostDotMenu, PostMenu, ThreadFooter, }; diff --git a/e2e-tests/playwright/support/ui/pages/boards_create.ts b/e2e-tests/playwright/support/ui/pages/boards_create.ts deleted file mode 100644 index f49aa163df8..00000000000 --- a/e2e-tests/playwright/support/ui/pages/boards_create.ts +++ /dev/null @@ -1,47 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import {expect, Page} from '@playwright/test'; - -import {components} from '@e2e-support/ui/components'; - -export default class BoardsCreatePage { - readonly boards = 'Boards'; - readonly page: Page; - - readonly globalHeader; - - readonly createBoardHeading; - readonly createEmptyBoardButton; - readonly useTemplateButton; - - constructor(page: Page) { - this.page = page; - this.globalHeader = new components.GlobalHeader(this.page.locator('#global-header')); - this.createBoardHeading = page.getByRole('heading', {name: 'Create a board'}); - this.createEmptyBoardButton = page.getByRole('button', {name: ' Create an empty board'}); - this.useTemplateButton = page.getByRole('button', {name: 'Use this template'}); - } - - async goto(teamId = '') { - let boardsUrl = '/boards'; - if (teamId) { - boardsUrl += `/team/${teamId}`; - } - - await this.page.goto(boardsUrl); - } - - async toBeVisible() { - await this.globalHeader.toBeVisible(this.boards); - await expect(this.createEmptyBoardButton).toBeVisible(); - await expect(this.useTemplateButton).toBeVisible(); - await expect(this.createBoardHeading).toBeVisible(); - } - - async createEmptyBoard() { - await this.createEmptyBoardButton.click(); - } -} - -export {BoardsCreatePage}; diff --git a/e2e-tests/playwright/support/ui/pages/boards_view.ts b/e2e-tests/playwright/support/ui/pages/boards_view.ts deleted file mode 100644 index bc618e67f8f..00000000000 --- a/e2e-tests/playwright/support/ui/pages/boards_view.ts +++ /dev/null @@ -1,60 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import {expect, Page} from '@playwright/test'; - -import {components} from '@e2e-support/ui/components'; - -export default class BoardsViewPage { - readonly boards = 'Boards'; - readonly page: Page; - - readonly sidebar; - readonly globalHeader; - - readonly topHead; - readonly editableTitle; - readonly shareButton; - - constructor(page: Page) { - this.page = page; - this.sidebar = new components.BoardsSidebar(page.locator('.octo-sidebar')); - this.globalHeader = new components.GlobalHeader(this.page.locator('#global-header')); - this.topHead = page.locator('.top-head'); - this.editableTitle = this.topHead.getByPlaceholder('Untitled board'); - this.shareButton = page.getByRole('button', {name: '󰍁 Share'}); - } - - async goto(teamId = '', boardId = '', viewId = '', cardId = '') { - let boardsUrl = '/boards'; - if (teamId) { - boardsUrl += `/team/${teamId}`; - if (boardId) { - boardsUrl += `/${boardId}`; - if (viewId) { - boardsUrl += `/${viewId}`; - if (cardId) { - boardsUrl += `/${cardId}`; - } - } - } - } - - await this.page.goto(boardsUrl); - } - - async toBeVisible() { - await this.page.waitForLoadState('networkidle'); - await this.globalHeader.toBeVisible(this.boards); - await expect(this.shareButton).toBeVisible(); - await expect(this.topHead).toBeVisible(); - } - - async shouldHaveUntitledBoard() { - await this.editableTitle.isVisible(); - expect(await this.editableTitle.getAttribute('value')).toBe(''); - await expect(this.page.getByTitle('(Untitled Board)')).toBeVisible(); - } -} - -export {BoardsViewPage}; diff --git a/e2e-tests/playwright/support/ui/pages/channels.ts b/e2e-tests/playwright/support/ui/pages/channels.ts index 5ec00ae40bb..1031c51e127 100644 --- a/e2e-tests/playwright/support/ui/pages/channels.ts +++ b/e2e-tests/playwright/support/ui/pages/channels.ts @@ -3,36 +3,51 @@ import {Page} from '@playwright/test'; -import {waitUntil} from '@e2e-support/test_action'; import {components} from '@e2e-support/ui/components'; -import {duration, isSmallScreen} from '@e2e-support/util'; export default class ChannelsPage { readonly channels = 'Channels'; + readonly page: Page; - readonly postCreate; - readonly findChannelsModal; + readonly globalHeader; - readonly header; - readonly headerMobile; - readonly appBar; + readonly centerView; readonly sidebarLeft; readonly sidebarRight; - readonly postDotMenu; + readonly appBar; + + readonly findChannelsModal; readonly deletePostModal; + readonly postDotMenu; + readonly postReminderMenu; + + readonly emojiGifPickerPopup; + constructor(page: Page) { this.page = page; - this.postCreate = new components.ChannelsPostCreate(page.locator('#post-create')); - this.findChannelsModal = new components.FindChannelsModal(page.getByRole('dialog', {name: 'Find Channels'})); + + // The main areas of the app this.globalHeader = new components.GlobalHeader(page.locator('#global-header')); - this.header = new components.ChannelsHeader(page.locator('.channel-header')); - this.headerMobile = new components.ChannelsHeaderMobile(page.locator('.navbar')); - this.appBar = new components.ChannelsAppBar(page.locator('.app-bar')); + this.centerView = new components.ChannelsCenterView(page.getByTestId('channel_view')); this.sidebarLeft = new components.ChannelsSidebarLeft(page.locator('#SidebarContainer')); this.sidebarRight = new components.ChannelsSidebarRight(page.locator('#sidebar-right')); - this.postDotMenu = new components.PostDotMenu(page.getByRole('menu', {name: 'Post extra options'})); + this.appBar = new components.ChannelsAppBar(page.locator('.app-bar')); + + // Modals + this.findChannelsModal = new components.FindChannelsModal(page.getByRole('dialog', {name: 'Find Channels'})); this.deletePostModal = new components.DeletePostModal(page.locator('#deletePostModal')); + + // Menus + this.postDotMenu = new components.PostDotMenu(page.getByRole('menu', {name: 'Post extra options'})); + this.postReminderMenu = new components.PostReminderMenu(page.getByRole('menu', {name: 'Set a reminder for:'})); + + // Popovers + this.emojiGifPickerPopup = new components.EmojiGifPicker(page.locator('#emojiGifPicker')); + } + + async toBeVisible() { + await this.centerView.toBeVisible(); } async goto(teamName = '', channelName = '') { @@ -46,75 +61,6 @@ export default class ChannelsPage { await this.page.goto(channelsUrl); } - - async toBeVisible() { - if (!isSmallScreen(this.page.viewportSize())) { - await this.globalHeader.toBeVisible(this.channels); - } - await this.postCreate.toBeVisible(); - } - - async postMessage(message: string) { - await this.postCreate.input.waitFor(); - await this.postCreate.postMessage(message); - } - - async sendMessage() { - await this.postCreate.sendMessage(); - } - - async getFirstPost() { - await this.page.getByTestId('postView').first().waitFor(); - const post = await this.page.getByTestId('postView').first(); - return new components.ChannelsPost(post); - } - - async getLastPost() { - await this.page.getByTestId('postView').last().waitFor(); - const post = await this.page.getByTestId('postView').last(); - return new components.ChannelsPost(post); - } - - async getNthPost(index: number) { - await this.page.getByTestId('postView').nth(index).waitFor(); - const post = await this.page.getByTestId('postView').nth(index); - return new components.ChannelsPost(post); - } - - async getPostById(id: string) { - await this.page.locator(`[id="post_${id}"]`).waitFor(); - const post = await this.page.locator(`[id="post_${id}"]`); - return new components.ChannelsPost(post); - } - - async getRHSPostById(id: string) { - await this.page.locator(`[id="rhsPost_${id}"]`).waitFor(); - const post = await this.page.locator(`[id="rhsPost_${id}"]`); - return new components.ChannelsPost(post); - } - - async waitUntilLastPostContains(text: string, timeout = duration.ten_sec) { - await waitUntil( - async () => { - const post = await this.getLastPost(); - const content = await post.container.textContent(); - return content?.includes(text); - }, - {timeout} - ); - } - - async waitUntilPostWithIdContains(id: string, text: string, timeout = duration.ten_sec) { - await waitUntil( - async () => { - const post = await this.getPostById(id); - const content = await post.container.textContent(); - - return content?.includes(text); - }, - {timeout} - ); - } } export {ChannelsPage}; diff --git a/e2e-tests/playwright/support/ui/pages/index.ts b/e2e-tests/playwright/support/ui/pages/index.ts index f84d4a442db..215b7666b49 100644 --- a/e2e-tests/playwright/support/ui/pages/index.ts +++ b/e2e-tests/playwright/support/ui/pages/index.ts @@ -1,8 +1,6 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import {BoardsCreatePage} from './boards_create'; -import {BoardsViewPage} from './boards_view'; import {ChannelsPage} from './channels'; import {LandingLoginPage} from './landing_login'; import {LoginPage} from './login'; @@ -10,8 +8,6 @@ import {ResetPasswordPage} from './reset_password'; import {SignupPage} from './signup'; const pages = { - BoardsCreatePage, - BoardsViewPage, ChannelsPage, LandingLoginPage, LoginPage, @@ -19,4 +15,4 @@ const pages = { SignupPage, }; -export {pages, BoardsCreatePage, BoardsViewPage, ChannelsPage, LandingLoginPage, LoginPage, SignupPage}; +export {pages, ChannelsPage, LandingLoginPage, LoginPage, SignupPage}; diff --git a/e2e-tests/playwright/support/ui/pages/signup.ts b/e2e-tests/playwright/support/ui/pages/signup.ts index c3ec6a8f181..074db6fa5f2 100644 --- a/e2e-tests/playwright/support/ui/pages/signup.ts +++ b/e2e-tests/playwright/support/ui/pages/signup.ts @@ -44,7 +44,7 @@ export default class SignupPage { this.createAccountButton = page.locator('button:has-text("Create Account")'); this.emailError = page.locator('text=Please enter a valid email address'); this.usernameError = page.locator( - 'text=Usernames have to begin with a lowercase letter and be 3-22 characters long. You can use lowercase letters, numbers, periods, dashes, and underscores.' + 'text=Usernames have to begin with a lowercase letter and be 3-22 characters long. You can use lowercase letters, numbers, periods, dashes, and underscores.', ); this.passwordError = page.locator('text=Must be 5-64 characters long.'); diff --git a/e2e-tests/playwright/support/util.ts b/e2e-tests/playwright/support/util.ts index 9f1999625d8..fc591b940a1 100644 --- a/e2e-tests/playwright/support/util.ts +++ b/e2e-tests/playwright/support/util.ts @@ -2,7 +2,6 @@ // See LICENSE.txt for license information. import {v4 as uuidv4} from 'uuid'; -import {ViewportSize} from '@playwright/test'; const second = 1000; const minute = 60 * 1000; @@ -46,7 +45,3 @@ export function getRandomId(length = 7): string { export const defaultTeam = {name: 'ad-1', displayName: 'eligendi', type: 'O'}; export const illegalRe = /[/?<>\\:*|":&();]/g; - -export function isSmallScreen(viewport?: ViewportSize | {width: number; height: number} | null) { - return viewport?.width ? Boolean(viewport?.width <= 390) : true; -} diff --git a/e2e-tests/playwright/support/visual/index.ts b/e2e-tests/playwright/support/visual/index.ts index dbbfc8dd5ba..0728aaa331c 100644 --- a/e2e-tests/playwright/support/visual/index.ts +++ b/e2e-tests/playwright/support/visual/index.ts @@ -16,8 +16,8 @@ export async function matchSnapshot(testInfo: TestInfo, testArgs: TestArgs) { // eslint-disable-next-line no-console console.log( chalk.yellow( - `^ Warning: No visual test performed. Run in Linux or Playwright docker image to match snapshot.` - ) + `^ Warning: No visual test performed. Run in Linux or Playwright docker image to match snapshot.`, + ), ); return; } diff --git a/e2e-tests/playwright/test.config.ts b/e2e-tests/playwright/test.config.ts index 1149b84d715..7dc2bc23a56 100644 --- a/e2e-tests/playwright/test.config.ts +++ b/e2e-tests/playwright/test.config.ts @@ -4,8 +4,6 @@ import {Page, ViewportSize} from '@playwright/test'; import * as dotenv from 'dotenv'; -import {callsPluginId} from '@e2e-support/constant'; - dotenv.config(); export type TestArgs = { @@ -47,7 +45,7 @@ const config: TestConfig = { ensurePluginsInstalled: typeof process.env?.PW_ENSURE_PLUGINS_INSTALLED === 'string' ? process.env.PW_ENSURE_PLUGINS_INSTALLED.split(',') - : [callsPluginId], + : [], haClusterEnabled: parseBool(process.env.PW_HA_CLUSTER_ENABLED, false), haClusterNodeCount: parseNumber(process.env.PW_HA_CLUSTER_NODE_COUNT, 2), haClusterName: process.env.PW_HA_CLUSTER_NAME || 'mm_dev_cluster', diff --git a/e2e-tests/playwright/tests/accessibility/channels/intro_channel.spec.ts b/e2e-tests/playwright/tests/accessibility/channels/intro_channel.spec.ts index a8ce81c1966..60ace2a7157 100644 --- a/e2e-tests/playwright/tests/accessibility/channels/intro_channel.spec.ts +++ b/e2e-tests/playwright/tests/accessibility/channels/intro_channel.spec.ts @@ -3,19 +3,18 @@ import {expect, test} from '@e2e-support/test_fixture'; -test('Intro to channel', async ({pw, pages, axe}) => { - // Create and sign in a new user +test('Base channel accessibility', async ({pw, pages, axe}) => { + // # Create and sign in a new user const {user} = await pw.initSetup(); - // Log in a user in new browser context + // # Log in a user in new browser context const {page} = await pw.testBrowser.login(user); - // Visit a default channel page + // # Visit a default channel page const channelsPage = new pages.ChannelsPage(page); await channelsPage.goto(); await channelsPage.toBeVisible(); - await channelsPage.postMessage('hello'); - await channelsPage.sendMessage(); + await channelsPage.centerView.postCreate.postMessage('hello'); // # Analyze the page // Disable 'color-contrast' to be addressed by MM-53814 @@ -24,3 +23,136 @@ test('Intro to channel', async ({pw, pages, axe}) => { // * Should have no violation expect(accessibilityScanResults.violations).toHaveLength(0); }); + +test('Post actions tab support', async ({pw, pages, axe}) => { + // # Create and sign in a new user + const {user} = await pw.initSetup(); + + // # Log in a user in new browser context + const {page} = await pw.testBrowser.login(user); + + // # Visit a default channel page + const channelsPage = new pages.ChannelsPage(page); + await channelsPage.goto(); + await channelsPage.toBeVisible(); + await channelsPage.centerView.postCreate.postMessage('hello'); + + const post = await channelsPage.centerView.getLastPost(); + await post.hover(); + await post.postMenu.toBeVisible(); + + // # Open the dot menu + await post.postMenu.dotMenuButton.click(); + + // * Dot menu should be visible and have focused + await channelsPage.postDotMenu.toBeVisible(); + await expect(channelsPage.postDotMenu.container).toBeFocused(); + + // # Analyze the page + const accessibilityScanResults = await axe + .builder(page, {disableColorContrast: true}) + .include('.MuiMenu-list') + .analyze(); + + // * Should have no violation + expect(accessibilityScanResults.violations).toHaveLength(0); + + // * Should move focus to Reply after arrow down + await channelsPage.postDotMenu.container.press('ArrowDown'); + await expect(channelsPage.postDotMenu.replyMenuItem).toBeFocused(); + + // * Should move focus to Forward after arrow down + await channelsPage.postDotMenu.replyMenuItem.press('ArrowDown'); + await expect(channelsPage.postDotMenu.forwardMenuItem).toBeFocused(); + + // * Should move focus to Follow message after arrow down + await channelsPage.postDotMenu.forwardMenuItem.press('ArrowDown'); + await expect(channelsPage.postDotMenu.followMessageMenuItem).toBeFocused(); + + // * Should move focus to Mark as Unread after arrow down + await channelsPage.postDotMenu.followMessageMenuItem.press('ArrowDown'); + await expect(channelsPage.postDotMenu.markAsUnreadMenuItem).toBeFocused(); + + // * Should move focus to Remind after arrow down + await channelsPage.postDotMenu.markAsUnreadMenuItem.press('ArrowDown'); + await expect(channelsPage.postDotMenu.remindMenuItem).toBeFocused(); + + // * Should move focus to Save after arrow down + await channelsPage.postDotMenu.remindMenuItem.press('ArrowDown'); + await expect(channelsPage.postDotMenu.saveMenuItem).toBeFocused(); + + // * Should move focus to Pin to Channel after arrow down + await channelsPage.postDotMenu.saveMenuItem.press('ArrowDown'); + await expect(channelsPage.postDotMenu.pinToChannelMenuItem).toBeFocused(); + + // * Should move focus to Copy Link after arrow down + await channelsPage.postDotMenu.pinToChannelMenuItem.press('ArrowDown'); + await expect(channelsPage.postDotMenu.copyLinkMenuItem).toBeFocused(); + + // * Should move focus to Edit after arrow down + await channelsPage.postDotMenu.copyLinkMenuItem.press('ArrowDown'); + await expect(channelsPage.postDotMenu.editMenuItem).toBeFocused(); + + // * Should move focus to Copy Text after arrow down + await channelsPage.postDotMenu.editMenuItem.press('ArrowDown'); + await expect(channelsPage.postDotMenu.copyTextMenuItem).toBeFocused(); + + // * Should move focus to Delete after arrow down + await channelsPage.postDotMenu.copyTextMenuItem.press('ArrowDown'); + await expect(channelsPage.postDotMenu.deleteMenuItem).toBeFocused(); + + // * Then, should move focus back to Reply after arrow down + await channelsPage.postDotMenu.deleteMenuItem.press('ArrowDown'); + await expect(channelsPage.postDotMenu.replyMenuItem).toBeFocused(); + + // * Should move focus to Delete after arrow uo + await channelsPage.postDotMenu.container.press('ArrowUp'); + expect(await channelsPage.postDotMenu.deleteMenuItem).toBeFocused(); + + // # Set focus to Remind + await channelsPage.postDotMenu.remindMenuItem.focus(); + await expect(channelsPage.postDotMenu.remindMenuItem).toBeFocused(); + + // * Reminder menu should still be hidden + await expect(channelsPage.postReminderMenu.container).toBeHidden(); + + // # Press arrow right + await channelsPage.postDotMenu.remindMenuItem.press('ArrowRight'); + + // * Reminder menu should be visible and have focused + channelsPage.postReminderMenu.toBeVisible(); + await expect(channelsPage.postReminderMenu.container).toBeFocused(); + + // * Should move focus to 30 mins after arrow down + await channelsPage.postReminderMenu.container.press('ArrowDown'); + expect(await channelsPage.postReminderMenu.thirtyMinsMenuItem).toBeFocused(); + + // * Should move focus to 1 hour after arrow down + await channelsPage.postReminderMenu.thirtyMinsMenuItem.press('ArrowDown'); + expect(await channelsPage.postReminderMenu.oneHourMenuItem).toBeFocused(); + + // * Should move focus to 2 hours after arrow down + await channelsPage.postReminderMenu.oneHourMenuItem.press('ArrowDown'); + expect(await channelsPage.postReminderMenu.twoHoursMenuItem).toBeFocused(); + + // * Should move focus to Tomorrow after arrow down + await channelsPage.postReminderMenu.twoHoursMenuItem.press('ArrowDown'); + expect(await channelsPage.postReminderMenu.tomorrowMenuItem).toBeFocused(); + + // * Should move focus to Custom after arrow down + await channelsPage.postReminderMenu.tomorrowMenuItem.press('ArrowDown'); + expect(await channelsPage.postReminderMenu.customMenuItem).toBeFocused(); + + // * Then, should move focus back to 30 mins after arrow down + await channelsPage.postReminderMenu.customMenuItem.press('ArrowDown'); + expect(await channelsPage.postReminderMenu.thirtyMinsMenuItem).toBeFocused(); + + // * Should hide Reminder menu and focus to Remind menu after arrow left + await channelsPage.postReminderMenu.container.press('ArrowLeft'); + await expect(channelsPage.postReminderMenu.container).toBeHidden(); + await expect(channelsPage.postDotMenu.remindMenuItem).toBeFocused(); + + // * Should hide Dot menu of Escape + await channelsPage.postDotMenu.container.press('Escape'); + await expect(channelsPage.postDotMenu.container).toBeHidden(); +}); diff --git a/e2e-tests/playwright/tests/functional/boards/board-creation-and-set-up/create_empty_board.spec.ts b/e2e-tests/playwright/tests/functional/boards/board-creation-and-set-up/create_empty_board.spec.ts deleted file mode 100644 index 0967d8577c2..00000000000 --- a/e2e-tests/playwright/tests/functional/boards/board-creation-and-set-up/create_empty_board.spec.ts +++ /dev/null @@ -1,44 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import {expect, test} from '@e2e-support/test_fixture'; -import {shouldSkipInSmallScreen} from '@e2e-support/flag'; - -shouldSkipInSmallScreen(); - -test('MM-T4274 Create an Empty Board', async ({pw, pages}) => { - // Create and sign in a new user - const {user} = await pw.initSetup(); - - // Log in a user in new browser context - const {page} = await pw.testBrowser.login(user); - - // Visit a default channel page - const channelsPage = new pages.ChannelsPage(page); - await channelsPage.goto(); - await channelsPage.toBeVisible(); - - // Switch to Boards page - await channelsPage.globalHeader.switchProduct('Boards'); - - // Should have redirected to boards create page - const boardsCreatePage = new pages.BoardsCreatePage(page); - await boardsCreatePage.toBeVisible(); - - // Create empty board - await boardsCreatePage.createEmptyBoard(); - - // Should have redirected to boards view page - const boardsViewPage = new pages.BoardsViewPage(page); - await boardsViewPage.toBeVisible(); - await boardsViewPage.shouldHaveUntitledBoard(); - - // Type new title and hit enter - const title = 'Testing'; - await boardsViewPage.editableTitle.fill(title); - await boardsViewPage.editableTitle.press('Enter'); - - // Should update the title in heading and in sidebar - expect(await boardsViewPage.editableTitle.getAttribute('value')).toBe(title); - await boardsViewPage.sidebar.waitForTitle(title); -}); diff --git a/e2e-tests/playwright/tests/functional/channels/drafts/drafts_on_deleted_message.spec.ts b/e2e-tests/playwright/tests/functional/channels/drafts/drafts_on_deleted_message.spec.ts index 77a642252ed..d01c4493fc6 100644 --- a/e2e-tests/playwright/tests/functional/channels/drafts/drafts_on_deleted_message.spec.ts +++ b/e2e-tests/playwright/tests/functional/channels/drafts/drafts_on_deleted_message.spec.ts @@ -22,7 +22,7 @@ test('MM-T5435_1 Global Drafts link in sidebar should be hidden when another use createRandomPost({ channel_id: channel.id, user_id: adminUser.id, - }) + }), ); // # Log in as user in new browser context @@ -33,7 +33,7 @@ test('MM-T5435_1 Global Drafts link in sidebar should be hidden when another use await channelPage.goto(); await channelPage.toBeVisible(); - const lastPostByAdmin = await channelPage.getLastPost(); + const lastPostByAdmin = await channelPage.centerView.getLastPost(); await lastPostByAdmin.toBeVisible(); // # Open the last post sent by admin in RHS @@ -41,15 +41,14 @@ test('MM-T5435_1 Global Drafts link in sidebar should be hidden when another use await lastPostByAdmin.postMenu.toBeVisible(); await lastPostByAdmin.postMenu.reply(); - // # Write a message as a user + // # Post a message as a user const sidebarRight = channelPage.sidebarRight; await sidebarRight.toBeVisible(); - await sidebarRight.postMessage('Replying to a thread'); - await sidebarRight.sendMessage(); + await sidebarRight.postCreate.postMessage('Replying to a thread'); // # Write a message in the reply thread but don't send it now so that it becomes a draft const draftMessageByUser = 'I should be in drafts by User'; - await sidebarRight.postMessage(draftMessageByUser); + await sidebarRight.postCreate.writeMessage(draftMessageByUser); // # Close the RHS for draft to be saved await sidebarRight.close(); @@ -68,11 +67,11 @@ test('MM-T5435_1 Global Drafts link in sidebar should be hidden when another use await lastPostByAdmin.threadFooter.reply(); // * Verify drafts in user's textbox is still visible - const rhsTextboxValue = await sidebarRight.getInputValue(); + const rhsTextboxValue = await sidebarRight.postCreate.getInputValue(); expect(rhsTextboxValue).toBe(draftMessageByUser); // # Click on remove post - const deletedPostByAdminInRHS = await sidebarRight.getRHSPostById(adminPost.id); + const deletedPostByAdminInRHS = await sidebarRight.getPostById(adminPost.id); await deletedPostByAdminInRHS.remove(); // * Verify the drafts links should also be removed from sidebar @@ -91,11 +90,10 @@ test('MM-T5435_2 Global Drafts link in sidebar should be hidden when user delete await channelPage.toBeVisible(); // # Post a message in the channel - await channelPage.postMessage('Message which will be deleted'); - await channelPage.sendMessage(); + await channelPage.centerView.postCreate.postMessage('Message which will be deleted'); // # Start a thread by clicking on reply menuitem from post options menu - const post = await channelPage.getLastPost(); + const post = await channelPage.centerView.getLastPost(); await post.hover(); await post.postMenu.toBeVisible(); await post.postMenu.reply(); @@ -103,12 +101,11 @@ test('MM-T5435_2 Global Drafts link in sidebar should be hidden when user delete const sidebarRight = channelPage.sidebarRight; await sidebarRight.toBeVisible(); - // # Write a message in the thread - await sidebarRight.postMessage('Replying to a thread'); - await sidebarRight.sendMessage(); + // # Post a message in the thread + await sidebarRight.postCreate.postMessage('Replying to a thread'); // # Write a message in the reply thread but don't send it - await sidebarRight.postMessage('I should be in drafts'); + await sidebarRight.postCreate.writeMessage('I should be in drafts'); // # Close the RHS for draft to be saved await sidebarRight.close(); @@ -121,7 +118,7 @@ test('MM-T5435_2 Global Drafts link in sidebar should be hidden when user delete await post.postMenu.toBeVisible(); await post.postMenu.openDotMenu(); await channelPage.postDotMenu.toBeVisible(); - await channelPage.postDotMenu.delete(); + await channelPage.postDotMenu.deleteMenuItem.click(); // # Confirm the delete from the modal await channelPage.deletePostModal.toBeVisible(); diff --git a/e2e-tests/playwright/tests/functional/channels/gif_picker/gif_picker_search.spec.ts b/e2e-tests/playwright/tests/functional/channels/gif_picker/gif_picker_search.spec.ts new file mode 100644 index 00000000000..5a22f258559 --- /dev/null +++ b/e2e-tests/playwright/tests/functional/channels/gif_picker/gif_picker_search.spec.ts @@ -0,0 +1,97 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {expect, test} from '@e2e-support/test_fixture'; + +test('MM-T5445 Should search, select and post correct Gif when Gif picker is opened from center textbox', async ({ + pw, + pages, +}) => { + const {user} = await pw.initSetup(); + + // # Log in as a user in new browser context + const {page} = await pw.testBrowser.login(user); + + // # Visit default channel page + const channelPage = new pages.ChannelsPage(page); + await channelPage.goto(); + await channelPage.toBeVisible(); + + // # Open emoji/gif picker + await channelPage.centerView.postCreate.openEmojiPicker(); + await channelPage.emojiGifPickerPopup.toBeVisible(); + + // # Open gif tab + await channelPage.emojiGifPickerPopup.openGifTab(); + + // # Search for gif + await channelPage.emojiGifPickerPopup.searchGif('hello'); + + // # Select the first gif + const {img: firstSearchGifResult, alt: altOfFirstSearchGifResult} = + await channelPage.emojiGifPickerPopup.getNthGif(0); + await firstSearchGifResult.click(); + + // # Send the selected gif as a message + await channelPage.centerView.postCreate.sendMessage(); + + // * Verify that last message has the gif + const lastPost = await channelPage.centerView.getLastPost(); + await lastPost.toBeVisible(); + await expect(lastPost.body.getByLabel('file thumbnail')).toHaveAttribute('alt', altOfFirstSearchGifResult); +}); + +test('MM-T5446 Should search, select and post correct Gif when Gif picker is opened from RHS textbox', async ({ + pw, + pages, +}) => { + const {user} = await pw.initSetup(); + + // # Log in as a user in new browser context + const {page} = await pw.testBrowser.login(user); + + // # Visit default channel page + const channelPage = new pages.ChannelsPage(page); + await channelPage.goto(); + await channelPage.toBeVisible(); + + // # Send a message + await channelPage.centerView.postCreate.postMessage('Message to open RHS'); + + // # Open the last post sent in RHS + const lastPost = await channelPage.centerView.getLastPost(); + await lastPost.hover(); + await lastPost.postMenu.toBeVisible(); + await lastPost.postMenu.reply(); + + const sidebarRight = channelPage.sidebarRight; + await sidebarRight.toBeVisible(); + + // # Send a message in the thread + await sidebarRight.postCreate.toBeVisible(); + await sidebarRight.postCreate.writeMessage('Replying to a thread'); + await sidebarRight.postCreate.sendMessage(); + + // # Open emoji/gif picker + await sidebarRight.postCreate.openEmojiPicker(); + await channelPage.emojiGifPickerPopup.toBeVisible(); + + // # Open gif tab + await channelPage.emojiGifPickerPopup.openGifTab(); + + // # Search for gif + await channelPage.emojiGifPickerPopup.searchGif('hello'); + + // # Select the first gif + const {img: firstSearchGifResult, alt: altOfFirstSearchGifResult} = + await channelPage.emojiGifPickerPopup.getNthGif(0); + await firstSearchGifResult.click(); + + // # Send the selected gif as a message in the thread + await sidebarRight.postCreate.sendMessage(); + + // * Verify that last message has the gif + const lastPostInRHS = await sidebarRight.getLastPost(); + await lastPostInRHS.toBeVisible(); + await expect(lastPostInRHS.body.getByLabel('file thumbnail')).toHaveAttribute('alt', altOfFirstSearchGifResult); +}); diff --git a/e2e-tests/playwright/tests/functional/channels/search/find_channels.spec.ts b/e2e-tests/playwright/tests/functional/channels/search/find_channels.spec.ts index 542b4578920..13611b4b7ce 100644 --- a/e2e-tests/playwright/tests/functional/channels/search/find_channels.spec.ts +++ b/e2e-tests/playwright/tests/functional/channels/search/find_channels.spec.ts @@ -37,9 +37,7 @@ test('MM-T5424 Find channel search returns only 50 results when there are more t await channelsPage.toBeVisible(); // # Click on "Find channel" and type "test_channel" - if (pw.isSmallScreen()) { - await channelsPage.headerMobile.toggleSidebar(); - } + await channelsPage.centerView.headerMobile.toggleSidebar(); await channelsPage.sidebarLeft.findChannelButton.click(); await channelsPage.findChannelsModal.toBeVisible(); diff --git a/e2e-tests/playwright/tests/visual/boards/board_template.spec.ts b/e2e-tests/playwright/tests/visual/boards/board_template.spec.ts deleted file mode 100644 index 18668403497..00000000000 --- a/e2e-tests/playwright/tests/visual/boards/board_template.spec.ts +++ /dev/null @@ -1,24 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import {test} from '@e2e-support/test_fixture'; -import {shouldSkipInSmallScreen} from '@e2e-support/flag'; - -shouldSkipInSmallScreen(); - -test('Board template', async ({pw, pages, browserName, viewport}, testInfo) => { - // Create and sign in a new user - const {user} = await pw.initSetup(); - - // Log in a user in new browser context - const {page} = await pw.testBrowser.login(user); - - // Should have redirected to boards create page - const boardsCreatePage = new pages.BoardsCreatePage(page); - await boardsCreatePage.goto(); - await boardsCreatePage.toBeVisible(); - - // Match snapshot of create board page - const testArgs = {page, browserName, viewport}; - await pw.matchSnapshot(testInfo, testArgs); -}); diff --git a/e2e-tests/playwright/tests/visual/boards/board_template.spec.ts-snapshots/board-template-chrome-linux.png b/e2e-tests/playwright/tests/visual/boards/board_template.spec.ts-snapshots/board-template-chrome-linux.png deleted file mode 100644 index 987b4d5b8da..00000000000 Binary files a/e2e-tests/playwright/tests/visual/boards/board_template.spec.ts-snapshots/board-template-chrome-linux.png and /dev/null differ diff --git a/e2e-tests/playwright/tests/visual/boards/board_template.spec.ts-snapshots/board-template-firefox-linux.png b/e2e-tests/playwright/tests/visual/boards/board_template.spec.ts-snapshots/board-template-firefox-linux.png deleted file mode 100644 index 424d77357ea..00000000000 Binary files a/e2e-tests/playwright/tests/visual/boards/board_template.spec.ts-snapshots/board-template-firefox-linux.png and /dev/null differ diff --git a/e2e-tests/playwright/tests/visual/boards/board_template.spec.ts-snapshots/board-template-ipad-linux.png b/e2e-tests/playwright/tests/visual/boards/board_template.spec.ts-snapshots/board-template-ipad-linux.png deleted file mode 100644 index 2017d565c44..00000000000 Binary files a/e2e-tests/playwright/tests/visual/boards/board_template.spec.ts-snapshots/board-template-ipad-linux.png and /dev/null differ diff --git a/e2e-tests/playwright/tests/visual/boards/view_untitled_board.spec.ts b/e2e-tests/playwright/tests/visual/boards/view_untitled_board.spec.ts deleted file mode 100644 index f5a03b4ba9b..00000000000 --- a/e2e-tests/playwright/tests/visual/boards/view_untitled_board.spec.ts +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import {test} from '@e2e-support/test_fixture'; -import {shouldSkipInSmallScreen} from '@e2e-support/flag'; - -shouldSkipInSmallScreen(); - -test('View untitled board', async ({pw, pages, browserName, viewport}, testInfo) => { - // Create and sign in a new user - const {user} = await pw.initSetup(); - - // Log in a user in new browser context - const {page} = await pw.testBrowser.login(user); - - // Should have redirected to boards create page - const boardsCreatePage = new pages.BoardsCreatePage(page); - await boardsCreatePage.goto(); - await boardsCreatePage.toBeVisible(); - - // Create empty board - await boardsCreatePage.createEmptyBoard(); - - // Should have redirected to boards view page - const boardsViewPage = new pages.BoardsViewPage(page); - await boardsViewPage.toBeVisible(); - await boardsViewPage.shouldHaveUntitledBoard(); - - // Match snapshot of create board page - const testArgs = {page, browserName, viewport}; - await pw.matchSnapshot(testInfo, testArgs); -}); diff --git a/e2e-tests/playwright/tests/visual/boards/view_untitled_board.spec.ts-snapshots/view-untitled-board-chrome-linux.png b/e2e-tests/playwright/tests/visual/boards/view_untitled_board.spec.ts-snapshots/view-untitled-board-chrome-linux.png deleted file mode 100644 index 6ecbc24470b..00000000000 Binary files a/e2e-tests/playwright/tests/visual/boards/view_untitled_board.spec.ts-snapshots/view-untitled-board-chrome-linux.png and /dev/null differ diff --git a/e2e-tests/playwright/tests/visual/boards/view_untitled_board.spec.ts-snapshots/view-untitled-board-firefox-linux.png b/e2e-tests/playwright/tests/visual/boards/view_untitled_board.spec.ts-snapshots/view-untitled-board-firefox-linux.png deleted file mode 100644 index 0b93b4be184..00000000000 Binary files a/e2e-tests/playwright/tests/visual/boards/view_untitled_board.spec.ts-snapshots/view-untitled-board-firefox-linux.png and /dev/null differ diff --git a/e2e-tests/playwright/tests/visual/boards/view_untitled_board.spec.ts-snapshots/view-untitled-board-ipad-linux.png b/e2e-tests/playwright/tests/visual/boards/view_untitled_board.spec.ts-snapshots/view-untitled-board-ipad-linux.png deleted file mode 100644 index 4f4ae9d5f5d..00000000000 Binary files a/e2e-tests/playwright/tests/visual/boards/view_untitled_board.spec.ts-snapshots/view-untitled-board-ipad-linux.png and /dev/null differ diff --git a/e2e-tests/playwright/tests/visual/channels/intro_channel.spec.ts b/e2e-tests/playwright/tests/visual/channels/intro_channel.spec.ts index 86ebf18acc4..99ce06330ba 100644 --- a/e2e-tests/playwright/tests/visual/channels/intro_channel.spec.ts +++ b/e2e-tests/playwright/tests/visual/channels/intro_channel.spec.ts @@ -2,7 +2,6 @@ // See LICENSE.txt for license information. import {expect, test} from '@e2e-support/test_fixture'; -import {duration, wait} from '@e2e-support/util'; test('Intro to channel as regular user', async ({pw, pages, browserName, viewport}, testInfo) => { // Create and sign in a new user @@ -16,15 +15,8 @@ test('Intro to channel as regular user', async ({pw, pages, browserName, viewpor await channelsPage.goto(); await channelsPage.toBeVisible(); - // Wait for Boards' bot image to be loaded - const boardsWelcomePost = await channelsPage.getFirstPost(); - await expect(await boardsWelcomePost.getProfileImage('boards')).toBeVisible(); - await wait(duration.one_sec); - // Wait for Playbooks icon to be loaded in App bar, except in iphone - if (!pw.isSmallScreen()) { - await expect(channelsPage.appBar.playbooksIcon).toBeVisible(); - } + await expect(channelsPage.appBar.playbooksIcon).toBeVisible(); // Hide dynamic elements of Channels page await pw.hideDynamicChannelsContent(page); diff --git a/e2e-tests/playwright/tests/visual/channels/intro_channel.spec.ts-snapshots/intro-to-channel-as-regular-user-chrome-linux.png b/e2e-tests/playwright/tests/visual/channels/intro_channel.spec.ts-snapshots/intro-to-channel-as-regular-user-chrome-linux.png index 2c70317f108..ba7615108ab 100644 Binary files a/e2e-tests/playwright/tests/visual/channels/intro_channel.spec.ts-snapshots/intro-to-channel-as-regular-user-chrome-linux.png and b/e2e-tests/playwright/tests/visual/channels/intro_channel.spec.ts-snapshots/intro-to-channel-as-regular-user-chrome-linux.png differ diff --git a/e2e-tests/playwright/tests/visual/channels/intro_channel.spec.ts-snapshots/intro-to-channel-as-regular-user-firefox-linux.png b/e2e-tests/playwright/tests/visual/channels/intro_channel.spec.ts-snapshots/intro-to-channel-as-regular-user-firefox-linux.png index 09c9d935210..9ec3c425189 100644 Binary files a/e2e-tests/playwright/tests/visual/channels/intro_channel.spec.ts-snapshots/intro-to-channel-as-regular-user-firefox-linux.png and b/e2e-tests/playwright/tests/visual/channels/intro_channel.spec.ts-snapshots/intro-to-channel-as-regular-user-firefox-linux.png differ diff --git a/e2e-tests/playwright/tests/visual/channels/intro_channel.spec.ts-snapshots/intro-to-channel-as-regular-user-ipad-linux.png b/e2e-tests/playwright/tests/visual/channels/intro_channel.spec.ts-snapshots/intro-to-channel-as-regular-user-ipad-linux.png index 09894720473..5bbe7618783 100644 Binary files a/e2e-tests/playwright/tests/visual/channels/intro_channel.spec.ts-snapshots/intro-to-channel-as-regular-user-ipad-linux.png and b/e2e-tests/playwright/tests/visual/channels/intro_channel.spec.ts-snapshots/intro-to-channel-as-regular-user-ipad-linux.png differ diff --git a/e2e-tests/playwright/tests/visual/channels/intro_channel.spec.ts-snapshots/intro-to-channel-as-regular-user-iphone-linux.png b/e2e-tests/playwright/tests/visual/channels/intro_channel.spec.ts-snapshots/intro-to-channel-as-regular-user-iphone-linux.png deleted file mode 100644 index 1898570494d..00000000000 Binary files a/e2e-tests/playwright/tests/visual/channels/intro_channel.spec.ts-snapshots/intro-to-channel-as-regular-user-iphone-linux.png and /dev/null differ diff --git a/e2e-tests/playwright/tests/visual/common/landing_page.spec.ts-snapshots/landing-login-chrome-linux.png b/e2e-tests/playwright/tests/visual/common/landing_page.spec.ts-snapshots/landing-login-chrome-linux.png index de92fef79e6..1c5a0cdaddd 100644 Binary files a/e2e-tests/playwright/tests/visual/common/landing_page.spec.ts-snapshots/landing-login-chrome-linux.png and b/e2e-tests/playwright/tests/visual/common/landing_page.spec.ts-snapshots/landing-login-chrome-linux.png differ diff --git a/e2e-tests/playwright/tests/visual/common/landing_page.spec.ts-snapshots/landing-login-firefox-linux.png b/e2e-tests/playwright/tests/visual/common/landing_page.spec.ts-snapshots/landing-login-firefox-linux.png index 132a2ac8926..1856c983ef5 100644 Binary files a/e2e-tests/playwright/tests/visual/common/landing_page.spec.ts-snapshots/landing-login-firefox-linux.png and b/e2e-tests/playwright/tests/visual/common/landing_page.spec.ts-snapshots/landing-login-firefox-linux.png differ diff --git a/e2e-tests/playwright/tests/visual/common/landing_page.spec.ts-snapshots/landing-login-ipad-linux.png b/e2e-tests/playwright/tests/visual/common/landing_page.spec.ts-snapshots/landing-login-ipad-linux.png index b4457abe980..c67b985dfb1 100644 Binary files a/e2e-tests/playwright/tests/visual/common/landing_page.spec.ts-snapshots/landing-login-ipad-linux.png and b/e2e-tests/playwright/tests/visual/common/landing_page.spec.ts-snapshots/landing-login-ipad-linux.png differ diff --git a/e2e-tests/playwright/tests/visual/common/landing_page.spec.ts-snapshots/landing-login-iphone-linux.png b/e2e-tests/playwright/tests/visual/common/landing_page.spec.ts-snapshots/landing-login-iphone-linux.png deleted file mode 100644 index ea323eb8a71..00000000000 Binary files a/e2e-tests/playwright/tests/visual/common/landing_page.spec.ts-snapshots/landing-login-iphone-linux.png and /dev/null differ diff --git a/e2e-tests/playwright/tests/visual/common/login.spec.ts-snapshots/login-chrome-linux.png b/e2e-tests/playwright/tests/visual/common/login.spec.ts-snapshots/login-chrome-linux.png index 676fde4484c..c99ddac90a6 100644 Binary files a/e2e-tests/playwright/tests/visual/common/login.spec.ts-snapshots/login-chrome-linux.png and b/e2e-tests/playwright/tests/visual/common/login.spec.ts-snapshots/login-chrome-linux.png differ diff --git a/e2e-tests/playwright/tests/visual/common/login.spec.ts-snapshots/login-error-chrome-linux.png b/e2e-tests/playwright/tests/visual/common/login.spec.ts-snapshots/login-error-chrome-linux.png index b9d2760e305..dd9cbaad55e 100644 Binary files a/e2e-tests/playwright/tests/visual/common/login.spec.ts-snapshots/login-error-chrome-linux.png and b/e2e-tests/playwright/tests/visual/common/login.spec.ts-snapshots/login-error-chrome-linux.png differ diff --git a/e2e-tests/playwright/tests/visual/common/login.spec.ts-snapshots/login-error-firefox-linux.png b/e2e-tests/playwright/tests/visual/common/login.spec.ts-snapshots/login-error-firefox-linux.png index 4a3168df924..3e7e4791acc 100644 Binary files a/e2e-tests/playwright/tests/visual/common/login.spec.ts-snapshots/login-error-firefox-linux.png and b/e2e-tests/playwright/tests/visual/common/login.spec.ts-snapshots/login-error-firefox-linux.png differ diff --git a/e2e-tests/playwright/tests/visual/common/login.spec.ts-snapshots/login-error-ipad-linux.png b/e2e-tests/playwright/tests/visual/common/login.spec.ts-snapshots/login-error-ipad-linux.png index 1538d5f2a18..b66f7a50cb4 100644 Binary files a/e2e-tests/playwright/tests/visual/common/login.spec.ts-snapshots/login-error-ipad-linux.png and b/e2e-tests/playwright/tests/visual/common/login.spec.ts-snapshots/login-error-ipad-linux.png differ diff --git a/e2e-tests/playwright/tests/visual/common/login.spec.ts-snapshots/login-error-iphone-linux.png b/e2e-tests/playwright/tests/visual/common/login.spec.ts-snapshots/login-error-iphone-linux.png deleted file mode 100644 index 1e1cc4aafc9..00000000000 Binary files a/e2e-tests/playwright/tests/visual/common/login.spec.ts-snapshots/login-error-iphone-linux.png and /dev/null differ diff --git a/e2e-tests/playwright/tests/visual/common/login.spec.ts-snapshots/login-firefox-linux.png b/e2e-tests/playwright/tests/visual/common/login.spec.ts-snapshots/login-firefox-linux.png index 9e13bff69fe..ee7503143c3 100644 Binary files a/e2e-tests/playwright/tests/visual/common/login.spec.ts-snapshots/login-firefox-linux.png and b/e2e-tests/playwright/tests/visual/common/login.spec.ts-snapshots/login-firefox-linux.png differ diff --git a/e2e-tests/playwright/tests/visual/common/login.spec.ts-snapshots/login-ipad-linux.png b/e2e-tests/playwright/tests/visual/common/login.spec.ts-snapshots/login-ipad-linux.png index b44f4780193..a3b93c2012b 100644 Binary files a/e2e-tests/playwright/tests/visual/common/login.spec.ts-snapshots/login-ipad-linux.png and b/e2e-tests/playwright/tests/visual/common/login.spec.ts-snapshots/login-ipad-linux.png differ diff --git a/e2e-tests/playwright/tests/visual/common/login.spec.ts-snapshots/login-iphone-linux.png b/e2e-tests/playwright/tests/visual/common/login.spec.ts-snapshots/login-iphone-linux.png deleted file mode 100644 index 8b050c34a7d..00000000000 Binary files a/e2e-tests/playwright/tests/visual/common/login.spec.ts-snapshots/login-iphone-linux.png and /dev/null differ diff --git a/e2e-tests/playwright/tests/visual/common/signup_email.spec.ts-snapshots/signup-email-chrome-linux.png b/e2e-tests/playwright/tests/visual/common/signup_email.spec.ts-snapshots/signup-email-chrome-linux.png index c76d7b5ba65..c1ddad54e64 100644 Binary files a/e2e-tests/playwright/tests/visual/common/signup_email.spec.ts-snapshots/signup-email-chrome-linux.png and b/e2e-tests/playwright/tests/visual/common/signup_email.spec.ts-snapshots/signup-email-chrome-linux.png differ diff --git a/e2e-tests/playwright/tests/visual/common/signup_email.spec.ts-snapshots/signup-email-error-chrome-linux.png b/e2e-tests/playwright/tests/visual/common/signup_email.spec.ts-snapshots/signup-email-error-chrome-linux.png index 29fd43ace64..cfeb0670e61 100644 Binary files a/e2e-tests/playwright/tests/visual/common/signup_email.spec.ts-snapshots/signup-email-error-chrome-linux.png and b/e2e-tests/playwright/tests/visual/common/signup_email.spec.ts-snapshots/signup-email-error-chrome-linux.png differ diff --git a/e2e-tests/playwright/tests/visual/common/signup_email.spec.ts-snapshots/signup-email-error-firefox-linux.png b/e2e-tests/playwright/tests/visual/common/signup_email.spec.ts-snapshots/signup-email-error-firefox-linux.png index d57364459d0..29b04606801 100644 Binary files a/e2e-tests/playwright/tests/visual/common/signup_email.spec.ts-snapshots/signup-email-error-firefox-linux.png and b/e2e-tests/playwright/tests/visual/common/signup_email.spec.ts-snapshots/signup-email-error-firefox-linux.png differ diff --git a/e2e-tests/playwright/tests/visual/common/signup_email.spec.ts-snapshots/signup-email-error-ipad-linux.png b/e2e-tests/playwright/tests/visual/common/signup_email.spec.ts-snapshots/signup-email-error-ipad-linux.png index 65f5f5499e4..1c245f03538 100644 Binary files a/e2e-tests/playwright/tests/visual/common/signup_email.spec.ts-snapshots/signup-email-error-ipad-linux.png and b/e2e-tests/playwright/tests/visual/common/signup_email.spec.ts-snapshots/signup-email-error-ipad-linux.png differ diff --git a/e2e-tests/playwright/tests/visual/common/signup_email.spec.ts-snapshots/signup-email-error-iphone-linux.png b/e2e-tests/playwright/tests/visual/common/signup_email.spec.ts-snapshots/signup-email-error-iphone-linux.png deleted file mode 100644 index 30627d5d8d9..00000000000 Binary files a/e2e-tests/playwright/tests/visual/common/signup_email.spec.ts-snapshots/signup-email-error-iphone-linux.png and /dev/null differ diff --git a/e2e-tests/playwright/tests/visual/common/signup_email.spec.ts-snapshots/signup-email-firefox-linux.png b/e2e-tests/playwright/tests/visual/common/signup_email.spec.ts-snapshots/signup-email-firefox-linux.png index 9f255fded9a..6b418c51f3e 100644 Binary files a/e2e-tests/playwright/tests/visual/common/signup_email.spec.ts-snapshots/signup-email-firefox-linux.png and b/e2e-tests/playwright/tests/visual/common/signup_email.spec.ts-snapshots/signup-email-firefox-linux.png differ diff --git a/e2e-tests/playwright/tests/visual/common/signup_email.spec.ts-snapshots/signup-email-ipad-linux.png b/e2e-tests/playwright/tests/visual/common/signup_email.spec.ts-snapshots/signup-email-ipad-linux.png index 4303ec2f297..4e4e5976d53 100644 Binary files a/e2e-tests/playwright/tests/visual/common/signup_email.spec.ts-snapshots/signup-email-ipad-linux.png and b/e2e-tests/playwright/tests/visual/common/signup_email.spec.ts-snapshots/signup-email-ipad-linux.png differ diff --git a/e2e-tests/playwright/tests/visual/common/signup_email.spec.ts-snapshots/signup-email-iphone-linux.png b/e2e-tests/playwright/tests/visual/common/signup_email.spec.ts-snapshots/signup-email-iphone-linux.png deleted file mode 100644 index 588495a8b8e..00000000000 Binary files a/e2e-tests/playwright/tests/visual/common/signup_email.spec.ts-snapshots/signup-email-iphone-linux.png and /dev/null differ diff --git a/server/.editorconfig b/server/.editorconfig deleted file mode 100644 index d0d47befdd7..00000000000 --- a/server/.editorconfig +++ /dev/null @@ -1,27 +0,0 @@ -# http://editorconfig.org/ - -root = true - -[*] -end_of_line = lf -insert_final_newline = true -charset = utf-8 -trim_trailing_whitespace = true - -[*.{go,sql}] -indent_style = tab - -[*.{js,jsx,json,html}] -indent_style = space -indent_size = 4 - -[i18n/**.json] -indent_size = 2 - -[Makefile,*.mk] -indent_style = tab - -[*.md] -indent_style = space -indent_size = 4 -trim_trailing_whitespace = false diff --git a/server/Makefile b/server/Makefile index 705e0174899..bb2c437a121 100644 --- a/server/Makefile +++ b/server/Makefile @@ -1,4 +1,4 @@ -.PHONY: build package run stop run-client run-server run-haserver stop-haserver stop-client stop-server restart restart-server restart-client restart-haserver start-docker update-docker clean-dist clean nuke check-style check-client-style check-server-style check-unit-tests test dist run-client-tests setup-run-client-tests cleanup-run-client-tests test-client build-linux build-osx build-windows package-prep package-linux package-osx package-windows internal-test-web-client vet run-server-for-web-client-tests diff-config prepackaged-plugins prepackaged-binaries test-server test-server-ee test-server-quick test-server-race test-mmctl-unit test-mmctl-e2e test-mmctl test-mmctl-coverage mmctl-build mmctl-docs new-migration migrations-extract +.PHONY: build package run stop run-client run-server run-haserver stop-haserver stop-client stop-server restart restart-server restart-client restart-haserver start-docker update-docker clean-dist clean nuke check-style check-client-style check-server-style check-unit-tests test dist run-client-tests setup-run-client-tests cleanup-run-client-tests test-client build-linux build-osx build-windows package-prep package-linux package-osx package-windows internal-test-web-client vet run-server-for-web-client-tests diff-config prepackaged-plugins prepackaged-binaries test-server test-server-ee test-server-quick test-server-race test-mmctl-unit test-mmctl-e2e test-mmctl test-mmctl-coverage mmctl-build mmctl-docs new-migration migrations-extract test-public mocks-public ROOT := $(dir $(abspath $(lastword $(MAKEFILE_LIST)))) @@ -64,7 +64,7 @@ GOTESTSUM_JSONFILE ?= gotestsum.json # mmctl MMCTL_BUILD_TAGS = -MMCTL_TESTFLAGS = -timeout 30m -race +MMCTL_TESTFLAGS ?= -timeout 30m MMCTL_PKG = github.com/mattermost/mattermost/server/v8/cmd/mmctl/commands LDFLAGS += -X "$(MMCTL_PKG).gitCommit=$(BUILD_HASH)" LDFLAGS += -X "$(MMCTL_PKG).gitTreeState=$(GIT_TREESTATE)" @@ -151,33 +151,33 @@ DIST_PATH_OSX_ARM64=$(DIST_ROOT)/osx_arm64/mattermost DIST_PATH_WIN=$(DIST_ROOT)/windows/mattermost # Packages lists -TE_PACKAGES=$(shell $(GO) list ./... | grep -vE 'server/v8/cmd/mmctl') -SUITE_PACKAGES=$(shell $(GO) list ./...| grep -vE 'server/v8/cmd/mmctl') +TE_PACKAGES=$(shell $(GO) list ./public/...) $(shell $(GO) list ./... | grep -vE 'server/v8/cmd/mmctl') MMCTL_PACKAGES=$(shell $(GO) list ./... | grep -E 'server/v8/cmd/mmctl') TEMPLATES_DIR=templates # Plugins Packages -PLUGIN_PACKAGES ?= mattermost-plugin-antivirus-v0.1.2 +PLUGIN_PACKAGES ?= $(PLUGIN_PACKAGES:) +PLUGIN_PACKAGES += mattermost-plugin-antivirus-v1.0.0 PLUGIN_PACKAGES += mattermost-plugin-autolink-v1.4.0 PLUGIN_PACKAGES += mattermost-plugin-aws-SNS-v1.2.0 -PLUGIN_PACKAGES += mattermost-plugin-calls-v0.18.0 +PLUGIN_PACKAGES += mattermost-plugin-calls-v0.20.0 PLUGIN_PACKAGES += mattermost-plugin-channel-export-v1.0.0 PLUGIN_PACKAGES += mattermost-plugin-confluence-v1.3.0 PLUGIN_PACKAGES += mattermost-plugin-custom-attributes-v1.3.1 PLUGIN_PACKAGES += mattermost-plugin-github-v2.1.6 -PLUGIN_PACKAGES += mattermost-plugin-gitlab-v1.6.0 -PLUGIN_PACKAGES += mattermost-plugin-playbooks-v1.38.0 +PLUGIN_PACKAGES += mattermost-plugin-gitlab-v1.7.0 +PLUGIN_PACKAGES += mattermost-plugin-playbooks-v1.39.0 PLUGIN_PACKAGES += mattermost-plugin-jenkins-v1.1.0 -PLUGIN_PACKAGES += mattermost-plugin-jira-v3.2.5 +PLUGIN_PACKAGES += mattermost-plugin-jira-v4.0.0 PLUGIN_PACKAGES += mattermost-plugin-jitsi-v2.0.1 PLUGIN_PACKAGES += mattermost-plugin-nps-v1.3.2 PLUGIN_PACKAGES += mattermost-plugin-servicenow-v2.3.4 PLUGIN_PACKAGES += mattermost-plugin-todo-v0.6.1 PLUGIN_PACKAGES += mattermost-plugin-welcomebot-v1.3.0 -PLUGIN_PACKAGES += mattermost-plugin-zoom-v1.6.0 +PLUGIN_PACKAGES += mattermost-plugin-zoom-v1.6.2 PLUGIN_PACKAGES += mattermost-plugin-apps-v1.2.2 -PLUGIN_PACKAGES += focalboard-v7.11.2 +PLUGIN_PACKAGES += focalboard-v7.11.4 # Prepares the enterprise build if exists. The IGNORE stuff is a hack to get the Makefile to execute the commands outside a target ifeq ($(BUILD_ENTERPRISE_READY),true) @@ -210,8 +210,10 @@ endif include config.mk include build/*.mk +include public/Makefile LDFLAGS += -X "github.com/mattermost/mattermost/server/public/model.MockCWS=$(MM_ENABLE_CWS_MOCK)" +LDFLAGS += -X "github.com/mattermost/mattermost/server/public/model.MattermostGiphySdkKey=$(MM_GIPHY_SDK_KEY)" RUN_IN_BACKGROUND ?= ifeq ($(RUN_SERVER_IN_BACKGROUND),true) @@ -299,8 +301,8 @@ endif plugin-checker: $(GO) run $(GOFLAGS) ./public/plugin/checker -prepackaged-plugins: ## Populate the prepackaged-plugins directory - @echo Downloading prepackaged plugins +prepackaged-plugins: ## Populate the prepackaged-plugins directory. + @echo Downloading prepackaged plugins: $(PLUGIN_PACKAGES) mkdir -p prepackaged_plugins @cd prepackaged_plugins && for plugin_package in $(PLUGIN_PACKAGES) ; do \ curl -f -O -L https://plugins-store.test.mattermost.com/release/$$plugin_package.tar.gz; \ @@ -375,7 +377,7 @@ plugin-mocks: ## Creates mock files for plugins. einterfaces-mocks: ## Creates mock files for einterfaces. $(GO) install github.com/vektra/mockery/v2/...@v2.23.2 - $(GOBIN)/mockery --dir channels/einterfaces --all --output channels/einterfaces/mocks --note 'Regenerate this file using `make einterfaces-mocks`.' + $(GOBIN)/mockery --dir einterfaces --all --output einterfaces/mocks --note 'Regenerate this file using `make einterfaces-mocks`.' searchengine-mocks: ## Creates mock files for searchengines. $(GO) install github.com/vektra/mockery/v2/...@v2.23.2 @@ -395,7 +397,7 @@ email-mocks: ## Creates mocks for misc interfaces. $(GOBIN)/mockery --dir channels/app/email --name ServiceInterface --output channels/app/email/mocks --note 'Regenerate this file using `make email-mocks`.' platform-mocks: ## Creates mocks for platform interfaces. - $(GO) install github.com/vektra/mockery/v2/...@v2.14.0 + $(GO) install github.com/vektra/mockery/v2/...@v2.32.2 $(GOBIN)/mockery --dir channels/app/platform --name SuiteIFace --output channels/app/platform/mocks --note 'Regenerate this file using `make platform-mocks`.' mmctl-mocks: ## Creates mocks for mmctl @@ -405,7 +407,7 @@ mmctl-mocks: ## Creates mocks for mmctl pluginapi: ## Generates api and hooks glue code for plugins cd ./public && $(GO) generate $(GOFLAGS) ./plugin -mocks: store-mocks telemetry-mocks filestore-mocks ldap-mocks plugin-mocks einterfaces-mocks searchengine-mocks sharedchannel-mocks misc-mocks email-mocks platform-mocks mmctl-mocks +mocks: store-mocks telemetry-mocks filestore-mocks ldap-mocks plugin-mocks einterfaces-mocks searchengine-mocks sharedchannel-mocks misc-mocks email-mocks platform-mocks mmctl-mocks mocks-public layers: app-layers store-layers pluginapi @@ -441,7 +443,7 @@ gotestsum: test-compile: gotestsum ## Compile tests. @echo COMPILE TESTS - for package in $(SUITE_PACKAGES) $(EE_PACKAGES); do \ + for package in $(TE_PACKAGES) $(EE_PACKAGES); do \ $(GO) test $(GOFLAGS) -c $$package; \ done @@ -486,7 +488,7 @@ test-server: export GOTESTSUM_FORMAT := $(GOTESTSUM_FORMAT) test-server: export GOTESTSUM_JUNITFILE := $(GOTESTSUM_JUNITFILE) test-server: export GOTESTSUM_JSONFILE := $(GOTESTSUM_JSONFILE) test-server: test-server-pre - $(GOBIN)/gotestsum --rerun-fails=3 --packages="$(SUITE_PACKAGES) $(EE_PACKAGES)" -- $(GOFLAGS) -timeout=90m + $(GOBIN)/gotestsum --rerun-fails=3 --packages="$(TE_PACKAGES) $(EE_PACKAGES)" -- $(GOFLAGS)-timeout=90m ifneq ($(IS_CI),true) ifneq ($(MM_NO_DOCKER),true) ifneq ($(TEMP_DOCKER_SERVICES),) @@ -511,10 +513,10 @@ test-server-quick: export GOTESTSUM_JSONFILE := $(GOTESTSUM_JSONFILE) test-server-quick: check-prereqs-enterprise ## Runs only quick tests. ifeq ($(BUILD_ENTERPRISE_READY),true) @echo Running all tests - $(GOBIN)/gotestsum --packages="$(SUITE_PACKAGES) $(EE_PACKAGES)" -- $(GOFLAGS) -short + $(GOBIN)/gotestsum --packages="$(TE_PACKAGES) $(EE_PACKAGES)" -- $(GOFLAGS) -short else @echo Running only TE tests - $(GOBIN)/gotestsum --packages="$(SUITE_PACKAGES)" -- $(GOFLAGS) -short + $(GOBIN)/gotestsum --packages="$(TE_PACKAGES)" -- $(GOFLAGS) -short endif internal-test-web-client: ## Runs web client tests. @@ -540,7 +542,7 @@ test-data: run-server inject-test-data ## start a local instance and add test da inject-test-data: # add test data to the local instance. @if ! ./scripts/wait-for-system-start.sh; then \ - make stop; \ + make stop-server; \ fi @echo ServiceSettings.EnableLocalMode must be set to true. @@ -554,6 +556,8 @@ inject-test-data: # add test data to the local instance. @echo Login with a regular account username=user-1 password=SampleUs@r-1 @echo ======================================================================== + make stop-server + test-mmctl-unit: export MM_SERVER_PATH := $(MM_SERVER_PATH) test-mmctl-unit: export GOTESTSUM_FORMAT := $(GOTESTSUM_FORMAT) test-mmctl-unit: export GOTESTSUM_JUNITFILE := $(GOTESTSUM_JUNITFILE) @@ -571,17 +575,17 @@ test-mmctl-e2e: gotestsum start-docker $(GOBIN)/gotestsum --packages="$(MMCTL_PACKAGES)" -- -tags 'e2e $(MMCTL_BUILD_TAGS)' $(MMCTL_TESTFLAGS) test-mmctl: export MM_SERVER_PATH := $(MM_SERVER_PATH) -test-mmctl-e2e: export GOTESTSUM_FORMAT := $(GOTESTSUM_FORMAT) -test-mmctl-e2e: export GOTESTSUM_JUNITFILE := $(GOTESTSUM_JUNITFILE) -test-mmctl-e2e: export GOTESTSUM_JSONFILE := $(GOTESTSUM_JSONFILE) +test-mmctl: export GOTESTSUM_FORMAT := $(GOTESTSUM_FORMAT) +test-mmctl: export GOTESTSUM_JUNITFILE := $(GOTESTSUM_JUNITFILE) +test-mmctl: export GOTESTSUM_JSONFILE := $(GOTESTSUM_JSONFILE) test-mmctl: gotestsum start-docker @echo Running all mmctl tests $(GOBIN)/gotestsum --packages="$(MMCTL_PACKAGES)" -- -tags 'unit e2e $(MMCTL_BUILD_TAGS)' $(MMCTL_TESTFLAGS) test-mmctl-coverage: export MM_SERVER_PATH := $(MM_SERVER_PATH) -test-mmctl-e2e: export GOTESTSUM_FORMAT := $(GOTESTSUM_FORMAT) -test-mmctl-e2e: export GOTESTSUM_JUNITFILE := $(GOTESTSUM_JUNITFILE) -test-mmctl-e2e: export GOTESTSUM_JSONFILE := $(GOTESTSUM_JSONFILE) +test-mmctl-coverage: export GOTESTSUM_FORMAT := $(GOTESTSUM_FORMAT) +test-mmctl-coverage: export GOTESTSUM_JUNITFILE := $(GOTESTSUM_JUNITFILE) +test-mmctl-coverage: export GOTESTSUM_JSONFILE := $(GOTESTSUM_JSONFILE) test-mmctl-coverage: gotestsum start-docker @echo Running all mmctl tests with coverage $(GOBIN)/gotestsum --packages="$(MMCTL_PACKAGES)" -- -tags 'unit e2e $(MMCTL_BUILD_TAGS)' -coverprofile=mmctlcover.out $(MMCTL_TESTFLAGS) @@ -694,24 +698,24 @@ run-job-server: ## Runs the background job server. config-ldap: ## Configures LDAP. @echo Setting up configuration for local LDAP - @sed -i'' -e 's|"LdapServer": ".*"|"LdapServer": "localhost"|g' config/config.json - @sed -i'' -e 's|"BaseDN": ".*"|"BaseDN": "dc=mm,dc=test,dc=com"|g' config/config.json - @sed -i'' -e 's|"BindUsername": ".*"|"BindUsername": "cn=admin,dc=mm,dc=test,dc=com"|g' config/config.json - @sed -i'' -e 's|"BindPassword": ".*"|"BindPassword": "mostest"|g' config/config.json - @sed -i'' -e 's|"FirstNameAttribute": ".*"|"FirstNameAttribute": "cn"|g' config/config.json - @sed -i'' -e 's|"LastNameAttribute": ".*"|"LastNameAttribute": "sn"|g' config/config.json - @sed -i'' -e 's|"NicknameAttribute": ".*"|"NicknameAttribute": "cn"|g' config/config.json - @sed -i'' -e 's|"EmailAttribute": ".*"|"EmailAttribute": "mail"|g' config/config.json - @sed -i'' -e 's|"UsernameAttribute": ".*"|"UsernameAttribute": "uid"|g' config/config.json - @sed -i'' -e 's|"IdAttribute": ".*"|"IdAttribute": "uid"|g' config/config.json - @sed -i'' -e 's|"LoginIdAttribute": ".*"|"LoginIdAttribute": "uid"|g' config/config.json - @sed -i'' -e 's|"GroupDisplayNameAttribute": ".*"|"GroupDisplayNameAttribute": "cn"|g' config/config.json - @sed -i'' -e 's|"GroupIdAttribute": ".*"|"GroupIdAttribute": "entryUUID"|g' config/config.json + @sed -i'' -e 's|"LdapServer": ".*"|"LdapServer": "localhost"|g' ../config/config.json + @sed -i'' -e 's|"BaseDN": ".*"|"BaseDN": "dc=mm,dc=test,dc=com"|g' ../config/config.json + @sed -i'' -e 's|"BindUsername": ".*"|"BindUsername": "cn=admin,dc=mm,dc=test,dc=com"|g' ../config/config.json + @sed -i'' -e 's|"BindPassword": ".*"|"BindPassword": "mostest"|g' ../config/config.json + @sed -i'' -e 's|"FirstNameAttribute": ".*"|"FirstNameAttribute": "cn"|g' ../config/config.json + @sed -i'' -e 's|"LastNameAttribute": ".*"|"LastNameAttribute": "sn"|g' ../config/config.json + @sed -i'' -e 's|"NicknameAttribute": ".*"|"NicknameAttribute": "cn"|g' ../config/config.json + @sed -i'' -e 's|"EmailAttribute": ".*"|"EmailAttribute": "mail"|g' ../config/config.json + @sed -i'' -e 's|"UsernameAttribute": ".*"|"UsernameAttribute": "uid"|g' ../config/config.json + @sed -i'' -e 's|"IdAttribute": ".*"|"IdAttribute": "uid"|g' ../config/config.json + @sed -i'' -e 's|"LoginIdAttribute": ".*"|"LoginIdAttribute": "uid"|g' ../config/config.json + @sed -i'' -e 's|"GroupDisplayNameAttribute": ".*"|"GroupDisplayNameAttribute": "cn"|g' ../config/config.json + @sed -i'' -e 's|"GroupIdAttribute": ".*"|"GroupIdAttribute": "entryUUID"|g' ../config/config.json -config-reset: ## Resets the config/config.json file to the default. - @echo Resetting configuration to default +config-reset: ## Resets the config/config.json file to the default production values. + @echo Resetting configuration to production default rm -f config/config.json - OUTPUT_CONFIG=$(PWD)/config/config.json $(GO) $(GOFLAGS) run ./scripts/config_generator + OUTPUT_CONFIG=$(PWD)/config/config.json $(GO) $(GOFLAGS) run -tags production ./scripts/config_generator diff-config: ## Compares default configuration between two mattermost versions @./scripts/diff-config.sh @@ -811,7 +815,7 @@ ifeq ($(BUILD_ENTERPRISE_READY),true) endif mmctl-build: ## Compiles and generates the mmctl binary - go build -trimpath -ldflags '$(LDFLAGS)' -o $(GOBIN) ./cmd/mmctl + go build -trimpath -ldflags '$(LDFLAGS)' -o bin/mmctl ./cmd/mmctl mmctl-docs: ## Generate the mmctl docs rm -rf ./cmd/mmctl/docs @@ -828,3 +832,32 @@ migrations-extract: @echo Listing migration files @echo "# Autogenerated file to synchronize migrations sequence in the PR workflow, please do not edit.\n#" > channels/db/migrations/migrations.list find channels/db/migrations -maxdepth 2 -mindepth 2 | sort >> channels/db/migrations/migrations.list + +test-migration: + $(GO) install github.com/mattermost/morph/cmd/morph@latest + # apply postgres migrations to the new db + bin/morph apply up --driver postgres --dsn "${POSTGRES_DSN}" --path ${POSTGRES_MIGRATIONS_PATH} --number -1 + + # create temporary file for the config + @cp tests/template.load tests/temp.load + + # set required values for the pgloader config, these values are read from /build/dotenv/migration.env in the CI + # do for psql credentials + @sed -i'' -e 's|{{ .pg_user }}|${PG_USERNAME}|g' tests/temp.load + @sed -i'' -e 's|{{ .pg_password }}|${PG_PASSWORD}|g' tests/temp.load + @sed -i'' -e 's|{{ .target_schema }}|${PG_SCHEMA}|g' tests/temp.load + + # do for mysql credentials + @sed -i'' -e 's|{{ .mysql_user }}|${MYSQL_USERNAME}|g' tests/temp.load + @sed -i'' -e 's|{{ .mysql_password }}|${MYSQL_PASSWORD}|g' tests/temp.load + @sed -i'' -e 's|{{ .source_schema }}|${MYSQL_SCHEMA}|g' tests/temp.load + + # run pgloader and save the logs + pgloader tests/temp.load > migration.log + + $(GO) install github.com/mattermost/dbcmp/cmd/dbcmp@latest + # compare two database contents + # db_migrations differ due to a typo in the 92. migration name + # for now we exclude plugins such as playbooks and focalboard + # we also exlude systems table temporarily due to adding some keys while running the initial migration + bin/dbcmp --source "${MYSQL_DSN}" --target "${POSTGRES_DSN}" --exclude="db_migrations","ir_","focalboard","systems" diff --git a/server/build/Dockerfile b/server/build/Dockerfile index ff5f6c9ee2c..a555177876d 100644 --- a/server/build/Dockerfile +++ b/server/build/Dockerfile @@ -8,7 +8,7 @@ SHELL ["/bin/bash", "-o", "pipefail", "-c"] ENV PATH="/mattermost/bin:${PATH}" ARG PUID=2000 ARG PGID=2000 -ARG MM_PACKAGE="https://releases.mattermost.com/8.0.1/mattermost-8.0.1-linux-amd64.tar.gz?src=docker" +ARG MM_PACKAGE="https://releases.mattermost.com/9.0.0/mattermost-9.0.0-linux-amd64.tar.gz?src=docker" # # Install needed packages and indirect dependencies RUN apt-get update \ diff --git a/server/build/Dockerfile.buildenv b/server/build/Dockerfile.buildenv index beeb1291efa..720b18dbee2 100644 --- a/server/build/Dockerfile.buildenv +++ b/server/build/Dockerfile.buildenv @@ -1,3 +1,3 @@ -FROM golang:1.19.5@sha256:bb9811fad43a7d6fd2173248d8331b2dcf5ac9af20976b1937ecd214c5b8c383 +FROM golang:1.20.7-bullseye@sha256:c95c52c458a2678251e0769ea13c710b0c52814771c53a495f53b9e87f9c0e99 RUN apt-get update && apt-get install -y make git apt-transport-https ca-certificates curl software-properties-common build-essential zip xmlsec1 jq diff --git a/server/build/contrib/gitlab/.gitlab-ci.yml b/server/build/contrib/gitlab/.gitlab-ci.yml deleted file mode 100644 index 92818e9a5e6..00000000000 --- a/server/build/contrib/gitlab/.gitlab-ci.yml +++ /dev/null @@ -1,61 +0,0 @@ -lint: - allow_failure: true - image: golangci/golangci-lint - script: - - GO111MODULE=off GOBIN=$PWD/bin go get -u github.com/mattermost/mattermost-govet - - make config-reset - - make check-style - -test-quick: - image: docker - variables: - DOCKER_HOST: tcp://docker:2375 - DOCKER_TLS_CERTDIR: "" - TEST_DATABASE_MYSQL_DSN: "mmuser:mostest@tcp(mysql:3306)/mattermost_test?charset=utf8mb4,utf8&readTimeout=30s&writeTimeout=30s&multiStatements=true&maxAllowedPacket=4194304" - TEST_DATABASE_POSTGRESQL_DSN: "postgres://mmuser:mostest@postgres:5432/mattermost_test?sslmode=disable&connect_timeout=10" - TEST_DATABASE_MYSQL_ROOT_PASSWD: "mostest" - services: - - docker:dind - before_script: - - apk add --no-cache bash docker-compose go make - - make start-docker - script: - - docker run - --env MM_NO_DOCKER=true - --env-file "build/dotenv/test.env" - --mount "type=bind,source=$PWD,target=$PWD" - --network "$CI_PROJECT_NAME"_mm-test - docker - /bin/sh -c " - cd $PWD; - apk add --no-cache bash docker-compose go mailcap make; - make test-server-quick; - " - -test-full: - rules: - - if: '$CI_COMMIT_REF_NAME == "master"' - image: docker - variables: - DOCKER_HOST: tcp://docker:2375 - DOCKER_TLS_CERTDIR: "" - TEST_DATABASE_MYSQL_DSN: "mmuser:mostest@tcp(mysql:3306)/mattermost_test?charset=utf8mb4,utf8&readTimeout=30s&writeTimeout=30s&multiStatements=true&maxAllowedPacket=4194304" - TEST_DATABASE_POSTGRESQL_DSN: "postgres://mmuser:mostest@postgres:5432/mattermost_test?sslmode=disable&connect_timeout=10" - TEST_DATABASE_MYSQL_ROOT_PASSWD: "mostest" - services: - - docker:dind - before_script: - - apk add --no-cache bash docker-compose go make - - make start-docker - script: - - docker run - --env MM_NO_DOCKER=true - --env-file "build/dotenv/test.env" - --mount "type=bind,source=$PWD,target=$PWD" - --network "$CI_PROJECT_NAME"_mm-test - docker - /bin/sh -c " - cd $PWD; - apk add --no-cache bash docker-compose go mailcap make; - make test-server; - " diff --git a/server/build/contrib/gitlab/README.md b/server/build/contrib/gitlab/README.md deleted file mode 100644 index f94e7b5fe97..00000000000 --- a/server/build/contrib/gitlab/README.md +++ /dev/null @@ -1,7 +0,0 @@ -# GitLab CI/CD configuration for Mattermost - -The [.gitlab-ci.yml](./.gitlab-ci.yml) file in this directory provides a [GitLab CI/CD configuration file](https://docs.gitlab.com/ee/ci/yaml/gitlab_ci_yaml.html) that can be used to run linting and unit testing for the Mattermost application. - -## Usage - -To configure your GitLab instance to use this configuration file (and without having to move it to the base directory in this git repository), you can configure the [`CI_CONFIG_PATH` predefined variable](https://docs.gitlab.com/ee/ci/variables/predefined_variables.html) for the repository in GitLab with the configuration file path (`build/contrib/gitlab/.gitlab-ci.yml`). diff --git a/server/build/docker-compose.common.yml b/server/build/docker-compose.common.yml index 0e2fe5dde3a..d3935e515d2 100644 --- a/server/build/docker-compose.common.yml +++ b/server/build/docker-compose.common.yml @@ -113,13 +113,13 @@ services: volumes: - "./docker/keycloak:/setup" prometheus: - image: "prom/prometheus" + image: "prom/prometheus:v2.46.0" volumes: - "./docker/prometheus${IS_LINUX}.yml:/etc/prometheus/prometheus.yml" networks: - mm-test grafana: - image: "grafana/grafana" + image: "grafana/grafana:10.0.3" volumes: - "./docker/grafana/grafana.ini:/etc/grafana/grafana.ini" - "./docker/grafana/provisioning:/etc/grafana/provisioning" diff --git a/server/build/dotenv/migration.env b/server/build/dotenv/migration.env new file mode 100644 index 00000000000..20339301d90 --- /dev/null +++ b/server/build/dotenv/migration.env @@ -0,0 +1,16 @@ +GOBIN=/mattermost/bin +PG_USERNAME=mmuser +PG_PASSWORD=mostest +PG_SCHEMA=mattermost_test +MYSQL_USERNAME=root +MYSQL_PASSWORD=mostest +MYSQL_SCHEMA=mattermost_test +POSTGRES_MIGRATIONS_PATH=./channels/db/migrations/postgres +MYSQL_DSN=${MYSQL_USERNAME}:${MYSQL_PASSWORD}@tcp(mysql:3306)/${MYSQL_SCHEMA}?charset=utf8mb4,utf8&readTimeout=30s&writeTimeout=30s&multiStatements=true +POSTGRES_DSN=postgres://${PG_USERNAME}:${PG_PASSWORD}@postgres:5432/${PG_SCHEMA}?sslmode=disable + +MM_SERVICESETTINGS_ENABLELOCALMODE=true +MM_SQLSETTINGS_DRIVERNAME=mysql +MM_SQLSETTINGS_DATASOURCE=mmuser:mostest@tcp(mysql:3306)/mattermost_test?charset=utf8mb4,utf8&readTimeout=30s&writeTimeout=30s&multiStatements=true&maxAllowedPacket=4194304 +MAX_WAIT_SECONDS=500 +IS_CI=true diff --git a/server/build/dotenv/test.env b/server/build/dotenv/test.env index e40cb65af66..929c5607de4 100644 --- a/server/build/dotenv/test.env +++ b/server/build/dotenv/test.env @@ -1,7 +1,7 @@ TEST_DATABASE_MYSQL_DSN=mmuser:mostest@tcp(mysql:3306)/mattermost_test?charset=utf8mb4,utf8&readTimeout=30s&writeTimeout=30s&multiStatements=true&maxAllowedPacket=4194304 TEST_DATABASE_POSTGRESQL_DSN=postgres://mmuser:mostest@postgres:5432/mattermost_test?sslmode=disable&connect_timeout=10 TEST_DATABASE_MYSQL_ROOT_PASSWD=mostest -GOBIN=/mattermost/mattermost-server/bin +GOBIN=/mattermost/server/bin CI_INBUCKET_HOST=inbucket CI_MINIO_HOST=minio diff --git a/server/build/gitlab-dc.common.yml b/server/build/gitlab-dc.common.yml index 5f001629a8d..05cdd39edf0 100644 --- a/server/build/gitlab-dc.common.yml +++ b/server/build/gitlab-dc.common.yml @@ -47,7 +47,7 @@ services: http.cors.allow-headers: "X-Requested-With,X-Auth-Token,Content-Type,Content-Length,Authorization" http.cors.allow-credentials: "true" transport.host: "127.0.0.1" - ES_JAVA_OPTS: "-Xms512m -Xmx512m" + ES_JAVA_OPTS: "-Xms512m -Xmx2G" healthcheck: test: [ "CMD", "bash", "-o", "pipefail", "-c", "curl --silent localhost:9200/_cat/health | awk '{ print $$4 }' | grep -qE '^green$$'" ] interval: 10s @@ -68,6 +68,7 @@ services: KEYCLOAK_PASSWORD: mostest DB_VENDOR: h2 KEYCLOAK_IMPORT: /setup/realm.json + JAVA_OPTS: "-Xms64m -Xmx2G -XX:MetaspaceSize=96M -XX:MaxMetaspaceSize=256m -Djava.net.preferIPv4Stack=true -Djboss.modules.system.pkgs=org.jboss.byteman -Djava.awt.headless=true" volumes: - "./docker/keycloak:/setup" healthcheck: diff --git a/server/build/gitlab-dc.mysql.yml b/server/build/gitlab-dc.mysql.yml index a681dbbb708..2c102abe5a9 100644 --- a/server/build/gitlab-dc.mysql.yml +++ b/server/build/gitlab-dc.mysql.yml @@ -96,5 +96,5 @@ services: networks: default: - external: - name: ${COMPOSE_PROJECT_NAME} + name: ${COMPOSE_PROJECT_NAME} + external: true diff --git a/server/build/gitlab-dc.postgres.yml b/server/build/gitlab-dc.postgres.yml index c27e978d0ad..b546a3ba5ab 100644 --- a/server/build/gitlab-dc.postgres.yml +++ b/server/build/gitlab-dc.postgres.yml @@ -94,5 +94,5 @@ services: networks: default: - external: - name: ${COMPOSE_PROJECT_NAME} + name: ${COMPOSE_PROJECT_NAME} + external: true diff --git a/server/build/gitlab-dc.schemamysql.yml b/server/build/gitlab-dc.schemamysql.yml index 317ce638854..d25001e3365 100644 --- a/server/build/gitlab-dc.schemamysql.yml +++ b/server/build/gitlab-dc.schemamysql.yml @@ -32,5 +32,5 @@ services: networks: default: - external: - name: ${COMPOSE_PROJECT_NAME} + name: ${COMPOSE_PROJECT_NAME} + external: true diff --git a/server/build/gitlab-dc.schemapostgres.yml b/server/build/gitlab-dc.schemapostgres.yml index 67b4d4a3023..b64e49374ac 100644 --- a/server/build/gitlab-dc.schemapostgres.yml +++ b/server/build/gitlab-dc.schemapostgres.yml @@ -30,5 +30,5 @@ services: networks: default: - external: - name: ${COMPOSE_PROJECT_NAME} + name: ${COMPOSE_PROJECT_NAME} + external: true diff --git a/server/channels/api4/api.go b/server/channels/api4/api.go index 868dd8d2c2a..d20fdd50fb7 100644 --- a/server/channels/api4/api.go +++ b/server/channels/api4/api.go @@ -81,8 +81,6 @@ type Routes struct { OAuthApps *mux.Router // 'api/v4/oauth/apps' OAuthApp *mux.Router // 'api/v4/oauth/apps/{app_id:[A-Za-z0-9]+}' - OpenGraph *mux.Router // 'api/v4/opengraph' - SAML *mux.Router // 'api/v4/saml' Compliance *mux.Router // 'api/v4/compliance' Cluster *mux.Router // 'api/v4/cluster' @@ -240,8 +238,6 @@ func Init(srv *app.Server) (*API, error) { api.BaseRoutes.ReactionByNameForPostForUser = api.BaseRoutes.PostForUser.PathPrefix("/reactions/{emoji_name:[A-Za-z0-9\\_\\-\\+]+}").Subrouter() - api.BaseRoutes.OpenGraph = api.BaseRoutes.APIRoot.PathPrefix("/opengraph").Subrouter() - api.BaseRoutes.Roles = api.BaseRoutes.APIRoot.PathPrefix("/roles").Subrouter() api.BaseRoutes.Schemes = api.BaseRoutes.APIRoot.PathPrefix("/schemes").Subrouter() @@ -294,7 +290,6 @@ func Init(srv *app.Server) (*API, error) { api.InitEmoji() api.InitOAuth() api.InitReaction() - api.InitOpenGraph() api.InitPlugin() api.InitRole() api.InitScheme() diff --git a/server/channels/api4/apitestlib.go b/server/channels/api4/apitestlib.go index 629557bee3c..963471074ac 100644 --- a/server/channels/api4/apitestlib.go +++ b/server/channels/api4/apitestlib.go @@ -29,8 +29,8 @@ import ( "github.com/mattermost/mattermost/server/public/model" "github.com/mattermost/mattermost/server/public/plugin/plugintest/mock" "github.com/mattermost/mattermost/server/public/shared/mlog" + "github.com/mattermost/mattermost/server/public/shared/request" "github.com/mattermost/mattermost/server/v8/channels/app" - "github.com/mattermost/mattermost/server/v8/channels/app/request" "github.com/mattermost/mattermost/server/v8/channels/store" "github.com/mattermost/mattermost/server/v8/channels/store/storetest/mocks" "github.com/mattermost/mattermost/server/v8/channels/testlib" diff --git a/server/channels/api4/bot.go b/server/channels/api4/bot.go index c7112a66103..e0ca6016f40 100644 --- a/server/channels/api4/bot.go +++ b/server/channels/api4/bot.go @@ -27,7 +27,7 @@ func (api *API) InitBot() { func createBot(c *Context, w http.ResponseWriter, r *http.Request) { var botPatch *model.BotPatch err := json.NewDecoder(r.Body).Decode(&botPatch) - if err != nil { + if err != nil || botPatch == nil { c.SetInvalidParamWithErr("bot", err) return } @@ -83,7 +83,7 @@ func patchBot(c *Context, w http.ResponseWriter, r *http.Request) { var botPatch *model.BotPatch err := json.NewDecoder(r.Body).Decode(&botPatch) - if err != nil { + if err != nil || botPatch == nil { c.SetInvalidParamWithErr("bot", err) return } @@ -135,13 +135,13 @@ func getBot(c *Context, w http.ResponseWriter, r *http.Request) { // Pretend like the bot doesn't exist at all to avoid revealing that the // user is a bot. It's kind of silly in this case, sine we created the bot, // but we don't have read bot permissions. - c.Err = model.MakeBotNotFoundError(botUserId) + c.Err = model.MakeBotNotFoundError("permissions", botUserId) return } } else { // Pretend like the bot doesn't exist at all, to avoid revealing that the // user is a bot. - c.Err = model.MakeBotNotFoundError(botUserId) + c.Err = model.MakeBotNotFoundError("permissions", botUserId) return } diff --git a/server/channels/api4/bot_test.go b/server/channels/api4/bot_test.go index dd97d581792..6c9eea4ed0d 100644 --- a/server/channels/api4/bot_test.go +++ b/server/channels/api4/bot_test.go @@ -133,6 +133,23 @@ func TestCreateBot(t *testing.T) { CheckErrorID(t, err, "api.context.permissions.app_error") }) + t.Run("create bot with null value", func(t *testing.T) { + th := Setup(t).InitBasic() + defer th.TearDown() + defer th.RestoreDefaultRolePermissions(th.SaveDefaultRolePermissions()) + + th.AddPermissionToRole(model.PermissionCreateBot.Id, model.TeamUserRoleId) + th.App.UpdateUserRoles(th.Context, th.BasicUser.Id, model.TeamUserRoleId, false) + th.App.UpdateConfig(func(cfg *model.Config) { + *cfg.ServiceSettings.EnableBotAccountCreation = true + }) + + var bot *model.Bot + + _, resp, err := th.Client.CreateBot(context.Background(), bot) + require.Error(t, err) + CheckBadRequestStatus(t, resp) + }) } func TestPatchBot(t *testing.T) { @@ -474,6 +491,34 @@ func TestPatchBot(t *testing.T) { require.Equal(t, th.BasicUser.Id, patchedBot.OwnerId) }) + + t.Run("patch with null bot", func(t *testing.T) { + th := Setup(t).InitBasic() + defer th.TearDown() + defer th.RestoreDefaultRolePermissions(th.SaveDefaultRolePermissions()) + + th.AddPermissionToRole(model.PermissionCreateBot.Id, model.TeamUserRoleId) + th.AddPermissionToRole(model.PermissionManageBots.Id, model.TeamUserRoleId) + th.App.UpdateUserRoles(th.Context, th.BasicUser.Id, model.TeamUserRoleId, false) + th.App.UpdateConfig(func(cfg *model.Config) { + *cfg.ServiceSettings.EnableBotAccountCreation = true + }) + + createdBot, resp, err := th.Client.CreateBot(context.Background(), &model.Bot{ + Username: GenerateTestUsername(), + DisplayName: "a bot", + Description: "bot", + }) + require.NoError(t, err) + CheckCreatedStatus(t, resp) + defer th.App.PermanentDeleteBot(createdBot.UserId) + + var botPatch *model.BotPatch + + _, resp1, err1 := th.Client.PatchBot(context.Background(), createdBot.UserId, botPatch) + require.Error(t, err1) + CheckBadRequestStatus(t, resp1) + }) } func TestGetBot(t *testing.T) { diff --git a/server/channels/api4/channel.go b/server/channels/api4/channel.go index 9b2e32d1c49..b703196a8ae 100644 --- a/server/channels/api4/channel.go +++ b/server/channels/api4/channel.go @@ -24,6 +24,7 @@ func (api *API) InitChannel() { api.BaseRoutes.Channels.Handle("/group/search", api.APISessionRequiredDisableWhenBusy(searchGroupChannels)).Methods("POST") api.BaseRoutes.Channels.Handle("/group", api.APISessionRequired(createGroupChannel)).Methods("POST") api.BaseRoutes.Channels.Handle("/members/{user_id:[A-Za-z0-9]+}/view", api.APISessionRequired(viewChannel)).Methods("POST") + api.BaseRoutes.Channels.Handle("/members/{user_id:[A-Za-z0-9]+}/mark_read", api.APISessionRequired(readMultipleChannels)).Methods("POST") api.BaseRoutes.Channels.Handle("/{channel_id:[A-Za-z0-9]+}/scheme", api.APISessionRequired(updateChannelScheme)).Methods("PUT") api.BaseRoutes.Channels.Handle("/stats/member_count", api.APISessionRequired(getChannelsMemberCount)).Methods("POST") @@ -59,6 +60,8 @@ func (api *API) InitChannel() { api.BaseRoutes.Channel.Handle("/members_minus_group_members", api.APISessionRequired(channelMembersMinusGroupMembers)).Methods("GET") api.BaseRoutes.Channel.Handle("/move", api.APISessionRequired(moveChannel)).Methods("POST") api.BaseRoutes.Channel.Handle("/member_counts_by_group", api.APISessionRequired(channelMemberCountsByGroup)).Methods("GET") + api.BaseRoutes.Channel.Handle("/common_teams", api.APISessionRequired(getGroupMessageMembersCommonTeams)).Methods("GET") + api.BaseRoutes.Channel.Handle("/convert_to_channel", api.APISessionRequired(convertGroupMessageToChannel)).Methods("POST") api.BaseRoutes.ChannelForUser.Handle("/unread", api.APISessionRequired(getChannelUnread)).Methods("GET") @@ -82,7 +85,7 @@ func (api *API) InitChannel() { func createChannel(c *Context, w http.ResponseWriter, r *http.Request) { var channel *model.Channel err := json.NewDecoder(r.Body).Decode(&channel) - if err != nil { + if err != nil || channel == nil { c.SetInvalidParamWithErr("channel", err) return } @@ -169,6 +172,10 @@ func updateChannel(c *Context, w http.ResponseWriter, r *http.Request) { c.Err = model.NewAppError("updateChannel", "api.channel.patch_update_channel.forbidden.app_error", nil, "", http.StatusForbidden) return } + if (channel.Name != "" && channel.Name != oldChannel.Name) || (channel.DisplayName != "" && channel.DisplayName != oldChannel.DisplayName) || (channel.Purpose != oldChannel.Purpose) { + c.Err = model.NewAppError("updateChannel", "api.channel.update_channel.update_direct_or_group_messages_not_allowed.app_error", nil, "", http.StatusBadRequest) + return + } default: c.Err = model.NewAppError("updateChannel", "api.channel.patch_update_channel.forbidden.app_error", nil, "", http.StatusForbidden) @@ -305,7 +312,7 @@ func patchChannel(c *Context, w http.ResponseWriter, r *http.Request) { } var patch *model.ChannelPatch err := json.NewDecoder(r.Body).Decode(&patch) - if err != nil { + if err != nil || patch == nil { c.SetInvalidParamWithErr("channel", err) return } @@ -341,6 +348,10 @@ func patchChannel(c *Context, w http.ResponseWriter, r *http.Request) { c.Err = model.NewAppError("patchChannel", "api.channel.patch_update_channel.forbidden.app_error", nil, "", http.StatusForbidden) return } + if (patch.Name != nil && *patch.Name != oldChannel.Name) || (patch.DisplayName != nil && *patch.DisplayName != oldChannel.DisplayName) || (patch.Purpose != nil && *patch.Purpose != oldChannel.Purpose) { + c.Err = model.NewAppError("patchChannel", "api.channel.patch_update_channel.update_direct_or_group_messages_not_allowed.app_error", nil, "", http.StatusBadRequest) + return + } default: c.Err = model.NewAppError("patchChannel", "api.channel.patch_update_channel.forbidden.app_error", nil, "", http.StatusForbidden) @@ -484,7 +495,7 @@ func createDirectChannel(c *Context, w http.ResponseWriter, r *http.Request) { func searchGroupChannels(c *Context, w http.ResponseWriter, r *http.Request) { var props *model.ChannelSearch err := json.NewDecoder(r.Body).Decode(&props) - if err != nil { + if err != nil || props == nil { c.SetInvalidParamWithErr("channel_search", err) return } @@ -1088,7 +1099,7 @@ func searchChannelsForTeam(c *Context, w http.ResponseWriter, r *http.Request) { var props *model.ChannelSearch err := json.NewDecoder(r.Body).Decode(&props) - if err != nil { + if err != nil || props == nil { c.SetInvalidParamWithErr("channel_search", err) return } @@ -1127,7 +1138,7 @@ func searchArchivedChannelsForTeam(c *Context, w http.ResponseWriter, r *http.Re var props *model.ChannelSearch err := json.NewDecoder(r.Body).Decode(&props) - if err != nil { + if err != nil || props == nil { c.SetInvalidParamWithErr("channel_search", err) return } @@ -1161,7 +1172,7 @@ func searchArchivedChannelsForTeam(c *Context, w http.ResponseWriter, r *http.Re func searchAllChannels(c *Context, w http.ResponseWriter, r *http.Request) { var props *model.ChannelSearch err := json.NewDecoder(r.Body).Decode(&props) - if err != nil { + if err != nil || props == nil { c.SetInvalidParamWithErr("channel_search", err) return } @@ -1537,6 +1548,32 @@ func viewChannel(c *Context, w http.ResponseWriter, r *http.Request) { } } +func readMultipleChannels(c *Context, w http.ResponseWriter, r *http.Request) { + c.RequireUserId() + + var channelIDs []string + err := json.NewDecoder(r.Body).Decode(&channelIDs) + if err != nil || len(channelIDs) == 0 { + c.SetInvalidParamWithErr("channel_ids", err) + return + } + + times, appErr := c.App.MarkChannelsAsViewed(c.AppContext, channelIDs, c.Params.UserId, c.AppContext.Session().Id, true, c.App.IsCRTEnabledForUser(c.AppContext, c.Params.UserId)) + if appErr != nil { + c.Err = appErr + return + } + + resp := &model.ChannelViewResponse{ + Status: "OK", + LastViewedAtTimes: times, + } + + if err := json.NewEncoder(w).Encode(resp); err != nil { + c.Logger.Warn("Error while writing response", mlog.Err(err)) + } +} + func updateChannelMemberRoles(c *Context, w http.ResponseWriter, r *http.Request) { c.RequireChannelId().RequireUserId() if c.Err != nil { @@ -2159,3 +2196,86 @@ func moveChannel(c *Context, w http.ResponseWriter, r *http.Request) { c.Logger.Warn("Error while writing response", mlog.Err(err)) } } + +func getGroupMessageMembersCommonTeams(c *Context, w http.ResponseWriter, r *http.Request) { + c.RequireChannelId() + if c.Err != nil { + return + } + + user, err := c.App.GetUser(c.AppContext.Session().UserId) + if err != nil { + c.Err = err + return + } + if user.IsGuest() { + c.Err = model.NewAppError("Api4.getGroupMessageMembersCommonTeams", "api.channel.gm_to_channel_conversion.not_allowed_for_user.request_error", nil, "userId="+c.AppContext.Session().UserId, http.StatusForbidden) + return + } + + if !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), c.Params.ChannelId, model.PermissionReadChannel) { + c.SetPermissionError(model.PermissionReadChannel) + return + } + + teams, appErr := c.App.GetGroupMessageMembersCommonTeams(c.AppContext, c.Params.ChannelId) + if appErr != nil { + c.Err = appErr + return + } + + if err := json.NewEncoder(w).Encode(teams); err != nil { + c.Logger.Warn("Error while writing response from getGroupMessageMembersCommonTeams", mlog.Err(err)) + } +} + +func convertGroupMessageToChannel(c *Context, w http.ResponseWriter, r *http.Request) { + c.RequireChannelId() + if c.Err != nil { + return + } + + var gmConversionRequest *model.GroupMessageConversionRequestBody + if err := json.NewDecoder(r.Body).Decode(&gmConversionRequest); err != nil || gmConversionRequest == nil { + c.SetInvalidParamWithErr("body", err) + return + } + + user, err := c.App.GetUser(c.AppContext.Session().UserId) + if err != nil { + c.Err = err + return + } + if user.IsGuest() { + c.Err = model.NewAppError("Api4.convertGroupMessageToChannel", "api.channel.gm_to_channel_conversion.not_allowed_for_user.request_error", nil, "userId="+c.AppContext.Session().UserId, http.StatusForbidden) + return + } + + if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), gmConversionRequest.TeamID, model.PermissionCreatePrivateChannel) { + c.SetPermissionError(model.PermissionCreatePrivateChannel) + return + } + + // The channel id the payload must be the same one as indicated in the URL. + if gmConversionRequest.ChannelID != c.Params.ChannelId { + c.SetInvalidParam("channel_id") + return + } + + auditRec := c.MakeAuditRecord("convertGroupMessageToChannel", audit.Fail) + defer c.LogAuditRec(auditRec) + audit.AddEventParameter(auditRec, "channel_id", gmConversionRequest.ChannelID) + audit.AddEventParameter(auditRec, "team_id", gmConversionRequest.TeamID) + audit.AddEventParameter(auditRec, "user_id", user.Id) + + updatedChannel, appErr := c.App.ConvertGroupMessageToChannel(c.AppContext, c.AppContext.Session().UserId, gmConversionRequest) + if appErr != nil { + c.Err = appErr + return + } + + auditRec.Success() + if err := json.NewEncoder(w).Encode(updatedChannel); err != nil { + c.Logger.Warn("Error while writing response from convertGroupMessageToChannel", mlog.Err(err)) + } +} diff --git a/server/channels/api4/channel_local.go b/server/channels/api4/channel_local.go index 65f6e204cb7..dd0e1c1febb 100644 --- a/server/channels/api4/channel_local.go +++ b/server/channels/api4/channel_local.go @@ -40,7 +40,7 @@ func (api *API) InitChannelLocal() { func localCreateChannel(c *Context, w http.ResponseWriter, r *http.Request) { var channel *model.Channel err := json.NewDecoder(r.Body).Decode(&channel) - if err != nil { + if err != nil || channel == nil { c.SetInvalidParamWithErr("channel", err) return } @@ -288,7 +288,7 @@ func localPatchChannel(c *Context, w http.ResponseWriter, r *http.Request) { var patch *model.ChannelPatch err := json.NewDecoder(r.Body).Decode(&patch) - if err != nil { + if err != nil || patch == nil { c.SetInvalidParamWithErr("channel", err) return } diff --git a/server/channels/api4/channel_test.go b/server/channels/api4/channel_test.go index 412e3a01692..3bb34555e58 100644 --- a/server/channels/api4/channel_test.go +++ b/server/channels/api4/channel_test.go @@ -125,6 +125,14 @@ func TestCreateChannel(t *testing.T) { require.NoError(t, err) }) + t.Run("null value", func(t *testing.T) { + var channel *model.Channel + _, resp, err = client.CreateChannel(context.Background(), channel) + + require.Error(t, err) + CheckBadRequestStatus(t, resp) + }) + // Test posting Garbage r, err := client.DoAPIPost(context.Background(), "/channels", "garbage") require.Error(t, err, "expected error") @@ -259,6 +267,58 @@ func TestUpdateChannel(t *testing.T) { require.Error(t, err) CheckBadRequestStatus(t, resp) }) + t.Run("Should block changes to name, display name or purpose for group messages", func(t *testing.T) { + user1 := th.CreateUser() + user2 := th.CreateUser() + user3 := th.CreateUser() + + client.Logout(context.Background()) + client.Login(context.Background(), user1.Email, user1.Password) + + groupChannel, _, err := client.CreateGroupChannel(context.Background(), []string{user1.Id, user2.Id, user3.Id}) + require.NoError(t, err) + + updatedChannel := &model.Channel{Id: groupChannel.Id, Name: "test name"} + _, resp, err := client.UpdateChannel(context.Background(), updatedChannel) + require.Error(t, err) + CheckBadRequestStatus(t, resp) + + updatedChannel2 := &model.Channel{Id: groupChannel.Id, DisplayName: "test display name"} + _, resp, err = client.UpdateChannel(context.Background(), updatedChannel2) + require.Error(t, err) + CheckBadRequestStatus(t, resp) + + updatedChannel3 := &model.Channel{Id: groupChannel.Id, Purpose: "test purpose"} + _, resp, err = client.UpdateChannel(context.Background(), updatedChannel3) + require.Error(t, err) + CheckBadRequestStatus(t, resp) + }) + + t.Run("Should block changes to name, display name or purpose for direct messages", func(t *testing.T) { + user1 := th.CreateUser() + user2 := th.CreateUser() + + client.Logout(context.Background()) + client.Login(context.Background(), user1.Email, user1.Password) + + directChannel, _, err := client.CreateDirectChannel(context.Background(), user1.Id, user2.Id) + require.NoError(t, err) + + updatedChannel := &model.Channel{Id: directChannel.Id, Name: "test name"} + _, resp, err := client.UpdateChannel(context.Background(), updatedChannel) + require.Error(t, err) + CheckBadRequestStatus(t, resp) + + updatedChannel2 := &model.Channel{Id: directChannel.Id, DisplayName: "test display name"} + _, resp, err = client.UpdateChannel(context.Background(), updatedChannel2) + require.Error(t, err) + CheckBadRequestStatus(t, resp) + + updatedChannel3 := &model.Channel{Id: directChannel.Id, Purpose: "test purpose"} + _, resp, err = client.UpdateChannel(context.Background(), updatedChannel3) + require.Error(t, err) + CheckBadRequestStatus(t, resp) + }) } func TestPatchChannel(t *testing.T) { @@ -267,6 +327,12 @@ func TestPatchChannel(t *testing.T) { client := th.Client team := th.BasicTeam + var nullPatch *model.ChannelPatch + + _, nullResp, err := client.PatchChannel(context.Background(), th.BasicChannel.Id, nullPatch) + require.Error(t, err) + CheckBadRequestStatus(t, nullResp) + patch := &model.ChannelPatch{ Name: new(string), DisplayName: new(string), @@ -365,6 +431,77 @@ func TestPatchChannel(t *testing.T) { _, resp, err = client.PatchChannel(context.Background(), directChannel.Id, channelPatch) require.Error(t, err) CheckForbiddenStatus(t, resp) + + t.Run("Should block changes to name, display name or purpose for group messages", func(t *testing.T) { + user1 := th.CreateUser() + user2 := th.CreateUser() + user3 := th.CreateUser() + + client.Logout(context.Background()) + client.Login(context.Background(), user1.Email, user1.Password) + + groupChannel, _, err := client.CreateGroupChannel(context.Background(), []string{user1.Id, user2.Id, user3.Id}) + require.NoError(t, err) + + groupChannelPatch := &model.ChannelPatch{ + Name: new(string), + } + *groupChannelPatch.Name = "testing" + _, resp, err := client.PatchChannel(context.Background(), groupChannel.Id, groupChannelPatch) + require.Error(t, err) + CheckBadRequestStatus(t, resp) + + groupChannelPatch2 := &model.ChannelPatch{ + DisplayName: new(string), + } + *groupChannelPatch2.DisplayName = "test display name" + _, resp, err = client.PatchChannel(context.Background(), groupChannel.Id, groupChannelPatch2) + require.Error(t, err) + CheckBadRequestStatus(t, resp) + + groupChannelPatch3 := &model.ChannelPatch{ + Purpose: new(string), + } + *groupChannelPatch3.Purpose = "test purpose" + _, resp, err = client.PatchChannel(context.Background(), groupChannel.Id, groupChannelPatch3) + require.Error(t, err) + CheckBadRequestStatus(t, resp) + }) + + t.Run("Should block changes to name, display name or purpose for direct messages", func(t *testing.T) { + user1 := th.CreateUser() + user2 := th.CreateUser() + + client.Logout(context.Background()) + client.Login(context.Background(), user1.Email, user1.Password) + + directChannel, _, err := client.CreateDirectChannel(context.Background(), user1.Id, user2.Id) + require.NoError(t, err) + + directChannelPatch := &model.ChannelPatch{ + Name: new(string), + } + *directChannelPatch.Name = "test" + _, resp, err := client.PatchChannel(context.Background(), directChannel.Id, directChannelPatch) + require.Error(t, err) + CheckBadRequestStatus(t, resp) + + directChannelPatch2 := &model.ChannelPatch{ + DisplayName: new(string), + } + *directChannelPatch2.DisplayName = "test display name" + _, resp, err = client.PatchChannel(context.Background(), directChannel.Id, directChannelPatch2) + require.Error(t, err) + CheckBadRequestStatus(t, resp) + + directChannelPatch3 := &model.ChannelPatch{ + Purpose: new(string), + } + *directChannelPatch3.Purpose = "test purpose" + _, resp, err = client.PatchChannel(context.Background(), directChannel.Id, directChannelPatch3) + require.Error(t, err) + CheckBadRequestStatus(t, resp) + }) } func TestChannelUnicodeNames(t *testing.T) { @@ -1285,6 +1422,15 @@ func TestSearchChannels(t *testing.T) { defer th.TearDown() client := th.Client + t.Run("Search using null value", func(t *testing.T) { + var nullSearch *model.ChannelSearch + + _, resp, err := client.SearchChannels(context.Background(), th.BasicTeam.Id, nullSearch) + + require.Error(t, err) + CheckBadRequestStatus(t, resp) + }) + search := &model.ChannelSearch{Term: th.BasicChannel.Name} channels, _, err := client.SearchChannels(context.Background(), th.BasicTeam.Id, search) @@ -1798,6 +1944,15 @@ func TestSearchGroupChannels(t *testing.T) { _, resp, err := client.SearchAllChannels(context.Background(), search) require.Error(t, err) CheckUnauthorizedStatus(t, resp) + + t.Run("search with null value", func(t *testing.T) { + var search *model.ChannelSearch + client.Login(context.Background(), th.BasicUser.Username, th.BasicUser.Password) + _, resp, err := client.SearchGroupChannels(context.Background(), search) + + require.Error(t, err) + CheckBadRequestStatus(t, resp) + }) } func TestDeleteChannel(t *testing.T) { @@ -2478,9 +2633,10 @@ func TestViewChannel(t *testing.T) { CheckBadRequestStatus(t, resp) view.ChannelId = "correctlysizedjunkdddfdfdf" - _, resp, err = client.ViewChannel(context.Background(), th.BasicUser.Id, view) - require.Error(t, err) - CheckBadRequestStatus(t, resp) + viewResult, _, err := client.ViewChannel(context.Background(), th.BasicUser.Id, view) + require.NoError(t, err) + require.Len(t, viewResult.LastViewedAtTimes, 0) + view.ChannelId = th.BasicChannel.Id member, _, err := client.GetChannelMember(context.Background(), th.BasicChannel.Id, th.BasicUser.Id, "") diff --git a/server/channels/api4/cloud.go b/server/channels/api4/cloud.go index 5eddeb72b8b..5d80b4bc8bb 100644 --- a/server/channels/api4/cloud.go +++ b/server/channels/api4/cloud.go @@ -140,7 +140,7 @@ func changeSubscription(c *Context, w http.ResponseWriter, r *http.Request) { } var subscriptionChange *model.SubscriptionChange - if err = json.Unmarshal(bodyBytes, &subscriptionChange); err != nil { + if err = json.Unmarshal(bodyBytes, &subscriptionChange); err != nil || subscriptionChange == nil { c.Err = model.NewAppError("Api4.changeSubscription", "api.cloud.app_error", nil, "", http.StatusBadRequest).Wrap(err) return } @@ -219,7 +219,7 @@ func requestCloudTrial(c *Context, w http.ResponseWriter, r *http.Request) { // this value will not be empty when both emails (user admin and CWS customer) are not business email and // a new business email was provided via the request business email modal var startTrialRequest *model.StartCloudTrialRequest - if err = json.Unmarshal(bodyBytes, &startTrialRequest); err != nil { + if err = json.Unmarshal(bodyBytes, &startTrialRequest); err != nil || startTrialRequest == nil { c.Err = model.NewAppError("Api4.requestCloudTrial", "api.cloud.app_error", nil, "", http.StatusBadRequest).Wrap(err) return } @@ -261,7 +261,7 @@ func validateBusinessEmail(c *Context, w http.ResponseWriter, r *http.Request) { var emailToValidate *model.ValidateBusinessEmailRequest err = json.Unmarshal(bodyBytes, &emailToValidate) - if err != nil { + if err != nil || emailToValidate == nil { c.Err = model.NewAppError("Api4.requestCloudTrial", "api.cloud.app_error", nil, "", http.StatusBadRequest).Wrap(err) return } @@ -353,7 +353,7 @@ func getSelfHostedProducts(c *Context, w http.ResponseWriter, r *http.Request) { if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleReadBilling) { sanitizedProducts := []model.UserFacingProduct{} err = json.Unmarshal(byteProductsData, &sanitizedProducts) - if err != nil { + if err != nil || sanitizedProducts == nil { c.Err = model.NewAppError("Api4.getSelfHostedProducts", "api.cloud.app_error", nil, "", http.StatusInternalServerError).Wrap(err) return } @@ -531,7 +531,7 @@ func updateCloudCustomer(c *Context, w http.ResponseWriter, r *http.Request) { } var customerInfo *model.CloudCustomerInfo - if err = json.Unmarshal(bodyBytes, &customerInfo); err != nil { + if err = json.Unmarshal(bodyBytes, &customerInfo); err != nil || customerInfo == nil { c.Err = model.NewAppError("Api4.updateCloudCustomer", "api.cloud.app_error", nil, "", http.StatusInternalServerError).Wrap(err) return } @@ -574,7 +574,7 @@ func updateCloudCustomerAddress(c *Context, w http.ResponseWriter, r *http.Reque } var address *model.Address - if err = json.Unmarshal(bodyBytes, &address); err != nil { + if err = json.Unmarshal(bodyBytes, &address); err != nil || address == nil { c.Err = model.NewAppError("Api4.updateCloudCustomerAddress", "api.cloud.app_error", nil, "", http.StatusInternalServerError).Wrap(err) return } @@ -656,7 +656,7 @@ func confirmCustomerPayment(c *Context, w http.ResponseWriter, r *http.Request) } var confirmRequest *model.ConfirmPaymentMethodRequest - if err = json.Unmarshal(bodyBytes, &confirmRequest); err != nil { + if err = json.Unmarshal(bodyBytes, &confirmRequest); err != nil || confirmRequest == nil { c.Err = model.NewAppError("Api4.confirmCustomerPayment", "api.cloud.app_error", nil, "", http.StatusInternalServerError).Wrap(err) return } @@ -762,7 +762,7 @@ func handleCWSWebhook(c *Context, w http.ResponseWriter, r *http.Request) { defer r.Body.Close() var event *model.CWSWebhookPayload - if err = json.Unmarshal(bodyBytes, &event); err != nil { + if err = json.Unmarshal(bodyBytes, &event); err != nil || event == nil { c.Err = model.NewAppError("Api4.handleCWSWebhook", "api.cloud.app_error", nil, err.Error(), http.StatusInternalServerError) return } @@ -881,7 +881,7 @@ func selfServeDeleteWorkspace(c *Context, w http.ResponseWriter, r *http.Request } var deleteRequest *model.WorkspaceDeletionRequest - if err = json.Unmarshal(bodyBytes, &deleteRequest); err != nil { + if err = json.Unmarshal(bodyBytes, &deleteRequest); err != nil || deleteRequest == nil { c.Err = model.NewAppError("Api4.selfServeDeleteWorkspace", "api.cloud.app_error", nil, err.Error(), http.StatusInternalServerError) return } diff --git a/server/channels/api4/command_test.go b/server/channels/api4/command_test.go index fddd0153446..48b865196fd 100644 --- a/server/channels/api4/command_test.go +++ b/server/channels/api4/command_test.go @@ -17,7 +17,7 @@ import ( "github.com/mattermost/mattermost/server/public/model" "github.com/mattermost/mattermost/server/public/shared/mlog" - "github.com/mattermost/mattermost/server/v8/channels/app/request" + "github.com/mattermost/mattermost/server/public/shared/request" ) func TestCreateCommand(t *testing.T) { diff --git a/server/channels/api4/data_retention.go b/server/channels/api4/data_retention.go index 78fad0afbab..64d7053e234 100644 --- a/server/channels/api4/data_retention.go +++ b/server/channels/api4/data_retention.go @@ -347,7 +347,7 @@ func searchChannelsInPolicy(c *Context, w http.ResponseWriter, r *http.Request) c.RequirePolicyId() var props *model.ChannelSearch err := json.NewDecoder(r.Body).Decode(&props) - if err != nil { + if err != nil || props == nil { c.SetInvalidParamWithErr("channel_search", err) return } diff --git a/server/channels/api4/drafts_test.go b/server/channels/api4/drafts_test.go index 87dbfef8bba..84e1ce993e9 100644 --- a/server/channels/api4/drafts_test.go +++ b/server/channels/api4/drafts_test.go @@ -7,6 +7,7 @@ import ( "context" "os" "testing" + "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -85,7 +86,6 @@ func TestUpsertDraft(t *testing.T) { } func TestGetDrafts(t *testing.T) { - t.Skip("MM-53452") os.Setenv("MM_FEATUREFLAGS_GLOBALDRAFTS", "true") defer os.Unsetenv("MM_FEATUREFLAGS_GLOBALDRAFTS") os.Setenv("MM_SERVICESETTINGS_ALLOWSYNCEDDRAFTS", "true") @@ -104,7 +104,6 @@ func TestGetDrafts(t *testing.T) { draft1 := &model.Draft{ CreateAt: 00001, - UpdateAt: 00001, UserId: user.Id, ChannelId: channel1.Id, Message: "draft1", @@ -112,7 +111,6 @@ func TestGetDrafts(t *testing.T) { draft2 := &model.Draft{ CreateAt: 11111, - UpdateAt: 32222, UserId: user.Id, ChannelId: channel2.Id, Message: "draft2", @@ -124,6 +122,9 @@ func TestGetDrafts(t *testing.T) { _, _, err := client.UpsertDraft(context.Background(), draft1) require.NoError(t, err) + // Wait a bit so the second draft gets a newer UpdateAt + time.Sleep(100 * time.Millisecond) + // upsert draft2 _, _, err = client.UpsertDraft(context.Background(), draft2) require.NoError(t, err) @@ -157,7 +158,6 @@ func TestGetDrafts(t *testing.T) { } func TestDeleteDraft(t *testing.T) { - t.Skip("MM-53452") os.Setenv("MM_FEATUREFLAGS_GLOBALDRAFTS", "true") defer os.Unsetenv("MM_FEATUREFLAGS_GLOBALDRAFTS") os.Setenv("MM_SERVICESETTINGS_ALLOWSYNCEDDRAFTS", "true") @@ -176,7 +176,6 @@ func TestDeleteDraft(t *testing.T) { draft1 := &model.Draft{ CreateAt: 00001, - UpdateAt: 00001, UserId: user.Id, ChannelId: channel1.Id, Message: "draft1", @@ -185,7 +184,6 @@ func TestDeleteDraft(t *testing.T) { draft2 := &model.Draft{ CreateAt: 11111, - UpdateAt: 32222, UserId: user.Id, ChannelId: channel2.Id, Message: "draft2", @@ -196,6 +194,9 @@ func TestDeleteDraft(t *testing.T) { _, _, err := client.UpsertDraft(context.Background(), draft1) require.NoError(t, err) + // Wait a bit so the second draft gets a newer UpdateAt + time.Sleep(100 * time.Millisecond) + // upsert draft2 _, _, err = client.UpsertDraft(context.Background(), draft2) require.NoError(t, err) diff --git a/server/channels/api4/group.go b/server/channels/api4/group.go index 8f923476fed..4be9d5989cb 100644 --- a/server/channels/api4/group.go +++ b/server/channels/api4/group.go @@ -117,6 +117,7 @@ func getGroup(c *Context, w http.ResponseWriter, r *http.Request) { group, appErr := c.App.GetGroup(c.Params.GroupId, &model.GetGroupOpts{ IncludeMemberCount: c.Params.IncludeMemberCount, + IncludeMemberIDs: c.Params.IncludeMemberIDs, }, restrictions) if appErr != nil { c.Err = appErr @@ -152,7 +153,7 @@ func createGroup(c *Context, w http.ResponseWriter, r *http.Request) { return } var group *model.GroupWithUserIds - if err := json.NewDecoder(r.Body).Decode(&group); err != nil { + if err := json.NewDecoder(r.Body).Decode(&group); err != nil || group == nil { c.SetInvalidParamWithErr("group", err) return } @@ -986,14 +987,20 @@ func getGroups(c *Context, w http.ResponseWriter, r *http.Request) { includeTimezones := r.URL.Query().Get("include_timezones") == "true" + // Include archived groups + includeArchived := r.URL.Query().Get("include_archived") == "true" + opts := model.GroupSearchOpts{ Q: c.Params.Q, IncludeMemberCount: c.Params.IncludeMemberCount, FilterAllowReference: c.Params.FilterAllowReference, + FilterArchived: c.Params.FilterArchived, FilterParentTeamPermitted: c.Params.FilterParentTeamPermitted, Source: source, FilterHasMember: c.Params.FilterHasMember, IncludeTimezones: includeTimezones, + IncludeMemberIDs: c.Params.IncludeMemberIDs, + IncludeArchived: includeArchived, } if teamID != "" { @@ -1145,15 +1152,19 @@ func deleteGroup(c *Context, w http.ResponseWriter, r *http.Request) { defer c.LogAuditRec(auditRec) audit.AddEventParameter(auditRec, "group_id", c.Params.GroupId) - _, err = c.App.DeleteGroup(c.Params.GroupId) + group, err = c.App.DeleteGroup(c.Params.GroupId) if err != nil { c.Err = err return } + b, jsonErr := json.Marshal(group) + if jsonErr != nil { + c.Err = model.NewAppError("Api4.deleteGroup", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(jsonErr) + return + } auditRec.Success() - - ReturnStatusOK(w) + w.Write(b) } func restoreGroup(c *Context, w http.ResponseWriter, r *http.Request) { @@ -1194,15 +1205,20 @@ func restoreGroup(c *Context, w http.ResponseWriter, r *http.Request) { defer c.LogAuditRec(auditRec) audit.AddEventParameter(auditRec, "group_id", c.Params.GroupId) - _, err = c.App.RestoreGroup(c.Params.GroupId) + restoredGroup, err := c.App.RestoreGroup(c.Params.GroupId) if err != nil { c.Err = err return } - auditRec.Success() + b, jsonErr := json.Marshal(restoredGroup) + if jsonErr != nil { + c.Err = model.NewAppError("Api4.restoreGroup", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(jsonErr) + return + } - ReturnStatusOK(w) + auditRec.Success() + w.Write(b) } func addGroupMembers(c *Context, w http.ResponseWriter, r *http.Request) { @@ -1223,13 +1239,13 @@ func addGroupMembers(c *Context, w http.ResponseWriter, r *http.Request) { } if group.Source != model.GroupSourceCustom { - c.Err = model.NewAppError("Api4.deleteGroup", "app.group.crud_permission", nil, "", http.StatusBadRequest) + c.Err = model.NewAppError("Api4.addGroupMembers", "app.group.crud_permission", nil, "", http.StatusBadRequest) return } appErr = licensedAndConfiguredForGroupBySource(c.App, model.GroupSourceCustom) if appErr != nil { - appErr.Where = "Api4.deleteGroup" + appErr.Where = "Api4.addGroupMembers" c.Err = appErr return } @@ -1240,7 +1256,7 @@ func addGroupMembers(c *Context, w http.ResponseWriter, r *http.Request) { } var newMembers *model.GroupModifyMembers - if err := json.NewDecoder(r.Body).Decode(&newMembers); err != nil { + if err := json.NewDecoder(r.Body).Decode(&newMembers); err != nil || newMembers == nil { c.SetInvalidParamWithErr("addGroupMembers", err) return } @@ -1282,13 +1298,13 @@ func deleteGroupMembers(c *Context, w http.ResponseWriter, r *http.Request) { } if group.Source != model.GroupSourceCustom { - c.Err = model.NewAppError("Api4.deleteGroup", "app.group.crud_permission", nil, "", http.StatusBadRequest) + c.Err = model.NewAppError("Api4.deleteGroupMembers", "app.group.crud_permission", nil, "", http.StatusBadRequest) return } appErr = licensedAndConfiguredForGroupBySource(c.App, model.GroupSourceCustom) if appErr != nil { - appErr.Where = "Api4.deleteGroup" + appErr.Where = "Api4.deleteGroupMembers" c.Err = appErr return } @@ -1299,7 +1315,7 @@ func deleteGroupMembers(c *Context, w http.ResponseWriter, r *http.Request) { } var deleteBody *model.GroupModifyMembers - if err := json.NewDecoder(r.Body).Decode(&deleteBody); err != nil { + if err := json.NewDecoder(r.Body).Decode(&deleteBody); err != nil || deleteBody == nil { c.SetInvalidParamWithErr("deleteGroupMembers", err) return } diff --git a/server/channels/api4/group_test.go b/server/channels/api4/group_test.go index 1585ce3debe..49a31fa85be 100644 --- a/server/channels/api4/group_test.go +++ b/server/channels/api4/group_test.go @@ -80,6 +80,10 @@ func TestCreateGroup(t *testing.T) { th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuProfessional, "ldap")) + _, resp, err := th.SystemAdminClient.CreateGroup(context.Background(), nil) + require.Error(t, err) + CheckBadRequestStatus(t, resp) + group, _, err := th.SystemAdminClient.CreateGroup(context.Background(), g) require.NoError(t, err) @@ -1291,6 +1295,21 @@ func TestGetGroups(t *testing.T) { // make sure it returned th.Group,not group assert.Equal(t, groups[0].Id, th.Group.Id) + // Test include_archived parameter + opts.IncludeArchived = true + groups, _, err = th.Client.GetGroups(context.Background(), opts) + assert.NoError(t, err) + assert.Len(t, groups, 2) + opts.IncludeArchived = false + + // Test returning only archived groups + opts.FilterArchived = true + groups, _, err = th.Client.GetGroups(context.Background(), opts) + assert.NoError(t, err) + assert.Len(t, groups, 1) + assert.Equal(t, groups[0].Id, group.Id) + opts.FilterArchived = false + opts.Source = model.GroupSourceCustom groups, _, err = th.Client.GetGroups(context.Background(), opts) assert.NoError(t, err) @@ -1567,6 +1586,11 @@ func TestAddMembersToGroup(t *testing.T) { th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuProfessional)) + //Empty group members returns bad request + _, resp, nullErr := th.SystemAdminClient.UpsertGroupMembers(context.Background(), group.Id, nil) + require.Error(t, nullErr) + CheckBadRequestStatus(t, resp) + groupMembers, response, upsertErr := th.SystemAdminClient.UpsertGroupMembers(context.Background(), group.Id, members) require.NoError(t, upsertErr) CheckOKStatus(t, response) @@ -1639,6 +1663,10 @@ func TestDeleteMembersFromGroup(t *testing.T) { th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuProfessional)) + _, resp, nullErr := th.SystemAdminClient.DeleteGroupMembers(context.Background(), group.Id, nil) + require.Error(t, nullErr) + CheckBadRequestStatus(t, resp) + groupMembers, response, deleteErr := th.SystemAdminClient.DeleteGroupMembers(context.Background(), group.Id, members) require.NoError(t, deleteErr) CheckOKStatus(t, response) diff --git a/server/channels/api4/handlers.go b/server/channels/api4/handlers.go index e5bbd3115b9..2c54e730a21 100644 --- a/server/channels/api4/handlers.go +++ b/server/channels/api4/handlers.go @@ -9,6 +9,8 @@ import ( "github.com/mattermost/gziphandler" "github.com/mattermost/mattermost/server/public/model" + "github.com/mattermost/mattermost/server/public/shared/mlog" + "github.com/mattermost/mattermost/server/v8/channels/app" "github.com/mattermost/mattermost/server/v8/channels/web" ) @@ -200,6 +202,17 @@ func (api *API) APILocal(h handlerFunc) http.Handler { return handler } +func (api *API) RateLimitedHandler(apiHandler http.Handler, settings model.RateLimitSettings) http.Handler { + settings.SetDefaults() + + rateLimiter, err := app.NewRateLimiter(&settings, []string{}) + if err != nil { + mlog.Error("getRateLimitedHandler", mlog.Err(err)) + return nil + } + return rateLimiter.RateLimitHandler(apiHandler) +} + func requireLicense(c *Context) *model.AppError { if c.App.Channels().License() == nil { err := model.NewAppError("", "api.license_error", nil, "", http.StatusNotImplemented) diff --git a/server/channels/api4/hosted_customer.go b/server/channels/api4/hosted_customer.go index f063b30c604..1c962a663ff 100644 --- a/server/channels/api4/hosted_customer.go +++ b/server/channels/api4/hosted_customer.go @@ -119,7 +119,7 @@ func selfHostedCustomer(c *Context, w http.ResponseWriter, r *http.Request) { } var form *model.SelfHostedCustomerForm - if err = json.Unmarshal(bodyBytes, &form); err != nil { + if err = json.Unmarshal(bodyBytes, &form); err != nil || form == nil { c.Err = model.NewAppError(where, "api.cloud.app_error", nil, "", http.StatusBadRequest).Wrap(err) return } diff --git a/server/channels/api4/job.go b/server/channels/api4/job.go index 1555fc4480d..d258f2479c3 100644 --- a/server/channels/api4/job.go +++ b/server/channels/api4/job.go @@ -31,7 +31,7 @@ func getJob(c *Context, w http.ResponseWriter, r *http.Request) { return } - job, err := c.App.GetJob(c.Params.JobId) + job, err := c.App.GetJob(c.AppContext, c.Params.JobId) if err != nil { c.Err = err return @@ -67,7 +67,7 @@ func downloadJob(c *Context, w http.ResponseWriter, r *http.Request) { return } - job, err := c.App.GetJob(c.Params.JobId) + job, err := c.App.GetJob(c.AppContext, c.Params.JobId) if err != nil { c.Err = err return @@ -126,7 +126,7 @@ func createJob(c *Context, w http.ResponseWriter, r *http.Request) { return } - rjob, err := c.App.CreateJob(&job) + rjob, err := c.App.CreateJob(c.AppContext, &job) if err != nil { c.Err = err return @@ -163,7 +163,7 @@ func getJobs(c *Context, w http.ResponseWriter, r *http.Request) { return } - jobs, appErr := c.App.GetJobsByTypesPage(validJobTypes, c.Params.Page, c.Params.PerPage) + jobs, appErr := c.App.GetJobsByTypesPage(c.AppContext, validJobTypes, c.Params.Page, c.Params.PerPage) if appErr != nil { c.Err = appErr return @@ -193,7 +193,7 @@ func getJobsByType(c *Context, w http.ResponseWriter, r *http.Request) { return } - jobs, appErr := c.App.GetJobsByTypePage(c.Params.JobType, c.Params.Page, c.Params.PerPage) + jobs, appErr := c.App.GetJobsByTypePage(c.AppContext, c.Params.JobType, c.Params.Page, c.Params.PerPage) if appErr != nil { c.Err = appErr return @@ -218,7 +218,7 @@ func cancelJob(c *Context, w http.ResponseWriter, r *http.Request) { defer c.LogAuditRec(auditRec) audit.AddEventParameter(auditRec, "job_id", c.Params.JobId) - job, err := c.App.GetJob(c.Params.JobId) + job, err := c.App.GetJob(c.AppContext, c.Params.JobId) if err != nil { c.Err = err return @@ -239,7 +239,7 @@ func cancelJob(c *Context, w http.ResponseWriter, r *http.Request) { return } - if err := c.App.CancelJob(c.Params.JobId); err != nil { + if err := c.App.CancelJob(c.AppContext, c.Params.JobId); err != nil { c.Err = err return } diff --git a/server/channels/api4/ldap.go b/server/channels/api4/ldap.go index b7dc07cbb8b..7312802785f 100644 --- a/server/channels/api4/ldap.go +++ b/server/channels/api4/ldap.go @@ -66,7 +66,7 @@ func syncLdap(c *Context, w http.ResponseWriter, r *http.Request) { return } - c.App.SyncLdap(opts.IncludeRemovedMembers) + c.App.SyncLdap(c.AppContext, opts.IncludeRemovedMembers) auditRec.Success() ReturnStatusOK(w) diff --git a/server/channels/api4/ldap_test.go b/server/channels/api4/ldap_test.go index bc09d51d686..46623ec6862 100644 --- a/server/channels/api4/ldap_test.go +++ b/server/channels/api4/ldap_test.go @@ -145,13 +145,14 @@ func TestSyncLdap(t *testing.T) { ldapMock := &mocks.LdapInterface{} mockCall := ldapMock.On( "StartSynchronizeJob", + mock.AnythingOfType("*request.Context"), mock.AnythingOfType("bool"), mock.AnythingOfType("bool"), ).Return(nil, nil) ready := make(chan bool) includeRemovedMembers := false mockCall.RunFn = func(args mock.Arguments) { - includeRemovedMembers = args[1].(bool) + includeRemovedMembers = args[2].(bool) ready <- true } th.App.Channels().Ldap = ldapMock diff --git a/server/channels/api4/notify_admin.go b/server/channels/api4/notify_admin.go index fd35b82759b..be87b3d7fcf 100644 --- a/server/channels/api4/notify_admin.go +++ b/server/channels/api4/notify_admin.go @@ -13,7 +13,7 @@ import ( func handleNotifyAdmin(c *Context, w http.ResponseWriter, r *http.Request) { var notifyAdminRequest *model.NotifyAdminToUpgradeRequest err := json.NewDecoder(r.Body).Decode(¬ifyAdminRequest) - if err != nil { + if err != nil || notifyAdminRequest == nil { c.SetInvalidParamWithErr("notifyAdminRequest", err) return } @@ -36,7 +36,7 @@ func handleTriggerNotifyAdminPosts(c *Context, w http.ResponseWriter, r *http.Re var notifyAdminRequest *model.NotifyAdminToUpgradeRequest err := json.NewDecoder(r.Body).Decode(¬ifyAdminRequest) - if err != nil { + if err != nil || notifyAdminRequest == nil { c.SetInvalidParamWithErr("notifyAdminRequest", err) return } diff --git a/server/channels/api4/notify_admin_test.go b/server/channels/api4/notify_admin_test.go index 3b144fc75b5..a9ceb6ed61e 100644 --- a/server/channels/api4/notify_admin_test.go +++ b/server/channels/api4/notify_admin_test.go @@ -13,6 +13,17 @@ import ( ) func TestNotifyAdmin(t *testing.T) { + t.Run("error when notifying with empty data", func(t *testing.T) { + th := Setup(t).InitBasic().InitLogin() + defer th.TearDown() + + statusCode, err := th.Client.NotifyAdmin(context.Background(), nil) + + require.Error(t, err) + require.Equal(t, http.StatusBadRequest, statusCode) + + }) + t.Run("error when plan is unknown when notifying on upgrade", func(t *testing.T) { th := Setup(t).InitBasic().InitLogin() defer th.TearDown() diff --git a/server/channels/api4/openGraph.go b/server/channels/api4/openGraph.go deleted file mode 100644 index a66d62016f6..00000000000 --- a/server/channels/api4/openGraph.go +++ /dev/null @@ -1,41 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package api4 - -import ( - "net/http" - - "github.com/mattermost/mattermost/server/public/model" - "github.com/mattermost/mattermost/server/public/shared/mlog" -) - -func (api *API) InitOpenGraph() { - api.BaseRoutes.OpenGraph.Handle("", api.APISessionRequired(getOpenGraphMetadata)).Methods("POST") -} - -func getOpenGraphMetadata(c *Context, w http.ResponseWriter, r *http.Request) { - if !*c.App.Config().ServiceSettings.EnableLinkPreviews { - c.Err = model.NewAppError("getOpenGraphMetadata", "api.post.link_preview_disabled.app_error", nil, "", http.StatusNotImplemented) - return - } - - props := model.StringInterfaceFromJSON(r.Body) - - url := "" - ok := false - if url, ok = props["url"].(string); url == "" || !ok { - c.SetInvalidParam("url") - return - } - - buf, err := c.App.GetOpenGraphMetadata(url) - if err != nil { - mlog.Warn("GetOpenGraphMetadata request failed", - mlog.String("requestURL", url), - mlog.Err(err)) - w.Write([]byte(`{"url": ""}`)) - return - } - w.Write(buf) -} diff --git a/server/channels/api4/openGraph_test.go b/server/channels/api4/openGraph_test.go deleted file mode 100644 index 52e65c0dbcc..00000000000 --- a/server/channels/api4/openGraph_test.go +++ /dev/null @@ -1,77 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package api4 - -import ( - "context" - "fmt" - "net/http" - "net/http/httptest" - "testing" - - "github.com/stretchr/testify/require" - - "github.com/mattermost/mattermost/server/public/model" -) - -func TestGetOpenGraphMetadata(t *testing.T) { - th := Setup(t).InitBasic() - defer th.TearDown() - - client := th.Client - - enableLinkPreviews := *th.App.Config().ServiceSettings.EnableLinkPreviews - allowedInternalConnections := *th.App.Config().ServiceSettings.AllowedUntrustedInternalConnections - defer func() { - th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableLinkPreviews = enableLinkPreviews }) - th.App.UpdateConfig(func(cfg *model.Config) { - cfg.ServiceSettings.AllowedUntrustedInternalConnections = &allowedInternalConnections - }) - }() - th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableLinkPreviews = true }) - th.App.UpdateConfig(func(cfg *model.Config) { - *cfg.ServiceSettings.AllowedUntrustedInternalConnections = "localhost,127.0.0.1" - }) - - ogDataCacheMissCount := 0 - - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - ogDataCacheMissCount++ - - if r.URL.Path == "/og-data/" { - fmt.Fprintln(w, ` - - - - - `) - } else if r.URL.Path == "/no-og-data/" { - fmt.Fprintln(w, ``) - } - })) - - for _, data := range [](map[string]any){ - {"path": "/og-data/", "title": "Test Title", "cacheMissCount": 1}, - {"path": "/no-og-data/", "title": "", "cacheMissCount": 2}, - - // Data should be cached for following - {"path": "/og-data/", "title": "Test Title", "cacheMissCount": 2}, - {"path": "/no-og-data/", "title": "", "cacheMissCount": 2}, - } { - - openGraph, _, err := client.OpenGraph(context.Background(), ts.URL+data["path"].(string)) - require.NoError(t, err) - - require.Equalf(t, openGraph["title"], data["title"].(string), - "OG data title mismatch for path \"%s\".") - - require.Equal(t, ogDataCacheMissCount, data["cacheMissCount"].(int), - "Cache miss count didn't match.") - } - - th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableLinkPreviews = false }) - _, resp, err := client.OpenGraph(context.Background(), ts.URL+"/og-data/") - require.Error(t, err) - CheckNotImplementedStatus(t, resp) -} diff --git a/server/channels/api4/plugin_test.go b/server/channels/api4/plugin_test.go index bcc4b19c806..d61129afd38 100644 --- a/server/channels/api4/plugin_test.go +++ b/server/channels/api4/plugin_test.go @@ -462,6 +462,7 @@ func TestDisableOnRemove(t *testing.T) { } func TestGetMarketplacePlugins(t *testing.T) { + th := Setup(t) defer th.TearDown() @@ -682,6 +683,7 @@ func TestGetMarketplacePlugins(t *testing.T) { } func TestGetInstalledMarketplacePlugins(t *testing.T) { + samplePlugins := []*model.MarketplacePlugin{ { BaseMarketplacePlugin: &model.BaseMarketplacePlugin{ @@ -825,6 +827,7 @@ func TestGetInstalledMarketplacePlugins(t *testing.T) { } func TestSearchGetMarketplacePlugins(t *testing.T) { + samplePlugins := []*model.MarketplacePlugin{ { BaseMarketplacePlugin: &model.BaseMarketplacePlugin{ @@ -950,6 +953,7 @@ func TestSearchGetMarketplacePlugins(t *testing.T) { } func TestGetLocalPluginInMarketplace(t *testing.T) { + th := Setup(t) defer th.TearDown() @@ -1111,6 +1115,7 @@ func TestGetLocalPluginInMarketplace(t *testing.T) { } func TestGetRemotePluginInMarketplace(t *testing.T) { + th := Setup(t) defer th.TearDown() @@ -1167,6 +1172,7 @@ func TestGetRemotePluginInMarketplace(t *testing.T) { } func TestGetPrepackagedPluginInMarketplace(t *testing.T) { + th := Setup(t) defer th.TearDown() @@ -1210,7 +1216,31 @@ func TestGetPrepackagedPluginInMarketplace(t *testing.T) { } env := th.App.GetPluginsEnvironment() - env.SetPrepackagedPlugins([]*plugin.PrepackagedPlugin{prepackagePlugin}) + env.SetPrepackagedPlugins([]*plugin.PrepackagedPlugin{prepackagePlugin}, nil) + + t.Run("prepackaged plugins are shown in Cloud", func(t *testing.T) { + th.App.UpdateConfig(func(cfg *model.Config) { + *cfg.PluginSettings.EnableRemoteMarketplace = true + *cfg.PluginSettings.EnableUploads = true + }) + + lic := th.App.Srv().License() + th.App.Srv().SetLicense(model.NewTestLicense("cloud")) + defer th.App.Srv().SetLicense(lic) + + plugins, _, err := th.SystemAdminClient.GetMarketplacePlugins(context.Background(), &model.MarketplacePluginFilter{}) + require.NoError(t, err) + + expectedPlugins := marketplacePlugins + expectedPlugins = append(expectedPlugins, &model.MarketplacePlugin{ + BaseMarketplacePlugin: &model.BaseMarketplacePlugin{ + Manifest: prepackagePlugin.Manifest, + }, + }) + + require.ElementsMatch(t, expectedPlugins, plugins) + require.Len(t, plugins, 2) + }) t.Run("get remote and prepackaged plugins", func(t *testing.T) { th.App.UpdateConfig(func(cfg *model.Config) { @@ -1263,7 +1293,7 @@ func TestGetPrepackagedPluginInMarketplace(t *testing.T) { } env := th.App.GetPluginsEnvironment() - env.SetPrepackagedPlugins([]*plugin.PrepackagedPlugin{newerPrepackagePlugin}) + env.SetPrepackagedPlugins([]*plugin.PrepackagedPlugin{newerPrepackagePlugin}, nil) plugins, _, err := th.SystemAdminClient.GetMarketplacePlugins(context.Background(), &model.MarketplacePluginFilter{}) require.NoError(t, err) @@ -1271,24 +1301,10 @@ func TestGetPrepackagedPluginInMarketplace(t *testing.T) { require.Len(t, plugins, 1) require.Equal(t, newerPrepackagePlugin.Manifest, plugins[0].Manifest) }) - - t.Run("prepackaged plugins are not shown in Cloud", func(t *testing.T) { - th.App.UpdateConfig(func(cfg *model.Config) { - *cfg.PluginSettings.EnableRemoteMarketplace = true - *cfg.PluginSettings.EnableUploads = true - }) - - th.App.Srv().SetLicense(model.NewTestLicense("cloud")) - - plugins, _, err := th.SystemAdminClient.GetMarketplacePlugins(context.Background(), &model.MarketplacePluginFilter{}) - require.NoError(t, err) - - require.ElementsMatch(t, marketplacePlugins, plugins) - require.Len(t, plugins, 1) - }) } func TestInstallMarketplacePlugin(t *testing.T) { + th := Setup(t).InitBasic() defer th.TearDown() @@ -1639,6 +1655,7 @@ func TestInstallMarketplacePlugin(t *testing.T) { } func TestInstallMarketplacePluginPrepackagedDisabled(t *testing.T) { + path, _ := fileutils.FindDir("tests") signatureFilename := "testplugin2.tar.gz.sig" diff --git a/server/channels/api4/post.go b/server/channels/api4/post.go index fe2b5535836..cb7ad741fc0 100644 --- a/server/channels/api4/post.go +++ b/server/channels/api4/post.go @@ -5,6 +5,7 @@ package api4 import ( "encoding/json" + "fmt" "net/http" "strconv" "time" @@ -75,6 +76,12 @@ func createPost(c *Context, w http.ResponseWriter, r *http.Request) { c.SetPermissionError(model.PermissionCreatePost) return } + if *c.App.Config().ServiceSettings.ExperimentalEnableHardenedMode { + if reservedProps := post.ContainsIntegrationsReservedProps(); len(reservedProps) > 0 && !c.AppContext.Session().IsIntegration() { + c.SetInvalidParamWithDetails("props", fmt.Sprintf("Cannot use props reserved for integrations. props: %v", reservedProps)) + return + } + } if post.CreateAt != 0 && !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) { post.CreateAt = 0 @@ -652,6 +659,7 @@ func getPostThread(c *Context, w http.ResponseWriter, r *http.Request) { // Either only fromCreateAt must be set, or both fromPost and fromCreateAt must be set if fromPost != "" && fromCreateAt == 0 { c.SetInvalidParam("if fromPost is set, then fromCreatAt must also be set") + return } direction := "" @@ -826,6 +834,13 @@ func updatePost(c *Context, w http.ResponseWriter, r *http.Request) { return } + if *c.App.Config().ServiceSettings.ExperimentalEnableHardenedMode { + if reservedProps := post.ContainsIntegrationsReservedProps(); len(reservedProps) > 0 && !c.AppContext.Session().IsIntegration() { + c.SetInvalidParamWithDetails("props", fmt.Sprintf("Cannot use props reserved for integrations. props: %v", reservedProps)) + return + } + } + if !c.App.SessionHasPermissionToChannelByPost(*c.AppContext.Session(), c.Params.PostId, model.PermissionEditPost) { c.SetPermissionError(model.PermissionEditPost) return @@ -887,6 +902,13 @@ func patchPost(c *Context, w http.ResponseWriter, r *http.Request) { audit.AddEventParameterAuditable(auditRec, "patch", &post) defer c.LogAuditRecWithLevel(auditRec, app.LevelContent) + if *c.App.Config().ServiceSettings.ExperimentalEnableHardenedMode { + if reservedProps := post.ContainsIntegrationsReservedProps(); len(reservedProps) > 0 && !c.AppContext.Session().IsIntegration() { + c.SetInvalidParamWithDetails("props", fmt.Sprintf("Cannot use props reserved for integrations. props: %v", reservedProps)) + return + } + } + // Updating the file_ids of a post is not a supported operation and will be ignored post.FileIds = nil diff --git a/server/channels/api4/post_test.go b/server/channels/api4/post_test.go index d3aaea401e1..11e1ff9b894 100644 --- a/server/channels/api4/post_test.go +++ b/server/channels/api4/post_test.go @@ -173,6 +173,26 @@ func TestCreatePost(t *testing.T) { } }) + t.Run("err with integrations-reserved props", func(t *testing.T) { + originalHardenedModeSetting := *th.App.Config().ServiceSettings.ExperimentalEnableHardenedMode + th.App.UpdateConfig(func(cfg *model.Config) { + *cfg.ServiceSettings.ExperimentalEnableHardenedMode = true + }) + + defer th.App.UpdateConfig(func(cfg *model.Config) { + *cfg.ServiceSettings.ExperimentalEnableHardenedMode = originalHardenedModeSetting + }) + + _, postResp, postErr := client.CreatePost(context.Background(), &model.Post{ + ChannelId: th.BasicChannel.Id, + Message: "with props", + Props: model.StringInterface{model.PostPropsFromWebhook: "true"}, + }) + + require.Error(t, postErr) + CheckBadRequestStatus(t, postResp) + }) + post.RootId = "" post.Type = model.PostTypeSystemGeneric _, resp, err := client.CreatePost(context.Background(), post) @@ -418,7 +438,7 @@ func TestCreatePostWithOAuthClient(t *testing.T) { Message: "test message", }) require.NoError(t, err) - assert.NotContains(t, post.GetProps(), "from_oauth_app", "contains from_oauth_app prop when not using OAuth client") + assert.NotContains(t, post.GetProps(), model.PostPropsFromOAuthApp, fmt.Sprintf("contains %s prop when not using OAuth client", model.PostPropsOverrideUsername)) client := th.CreateClient() client.SetOAuthToken(session.Token) @@ -428,7 +448,28 @@ func TestCreatePostWithOAuthClient(t *testing.T) { }) require.NoError(t, err) - assert.Contains(t, post.GetProps(), "from_oauth_app", "missing from_oauth_app prop when using OAuth client") + assert.Contains(t, post.GetProps(), model.PostPropsFromOAuthApp, fmt.Sprintf("missing %s prop when using OAuth client", model.PostPropsOverrideUsername)) + + t.Run("allow username and icon overrides", func(t *testing.T) { + originalHardenedModeSetting := *th.App.Config().ServiceSettings.ExperimentalEnableHardenedMode + th.App.UpdateConfig(func(cfg *model.Config) { + *cfg.ServiceSettings.ExperimentalEnableHardenedMode = true + }) + + defer th.App.UpdateConfig(func(cfg *model.Config) { + *cfg.ServiceSettings.ExperimentalEnableHardenedMode = originalHardenedModeSetting + }) + + post, _, err = client.CreatePost(context.Background(), &model.Post{ + ChannelId: th.BasicChannel.Id, + Message: "test message", + Props: model.StringInterface{model.PostPropsOverrideUsername: "newUsernameValue", model.PostPropsOverrideIconURL: "iconUrlOverrideValue"}, + }) + + require.NoError(t, err) + assert.Contains(t, post.GetProps(), model.PostPropsOverrideUsername, fmt.Sprintf("missing %s prop when using OAuth client", model.PostPropsOverrideUsername)) + assert.Contains(t, post.GetProps(), model.PostPropsOverrideIconURL, fmt.Sprintf("missing %s prop when using OAuth client", model.PostPropsOverrideIconURL)) + }) } func TestCreatePostEphemeral(t *testing.T) { @@ -1085,6 +1126,26 @@ func TestUpdatePost(t *testing.T) { CheckBadRequestStatus(t, resp) }) + t.Run("err with integrations-reserved props", func(t *testing.T) { + originalHardenedModeSetting := *th.App.Config().ServiceSettings.ExperimentalEnableHardenedMode + th.App.UpdateConfig(func(cfg *model.Config) { + *cfg.ServiceSettings.ExperimentalEnableHardenedMode = true + }) + + defer th.App.UpdateConfig(func(cfg *model.Config) { + *cfg.ServiceSettings.ExperimentalEnableHardenedMode = originalHardenedModeSetting + }) + + _, resp, err := client.UpdatePost(context.Background(), rpost.Id, &model.Post{ + ChannelId: th.BasicChannel.Id, + Message: "with props", + Props: model.StringInterface{model.PostPropsFromWebhook: "true"}, + }) + + require.Error(t, err) + CheckBadRequestStatus(t, resp) + }) + t.Run("logged out", func(t *testing.T) { client.Logout(context.Background()) _, resp, err := client.UpdatePost(context.Background(), rpost.Id, rpost) @@ -1295,6 +1356,33 @@ func TestPatchPost(t *testing.T) { CheckBadRequestStatus(t, resp) require.Equal(t, "api.post.update_post.permissions_time_limit.app_error", err.(*model.AppError).Id, "should be time limit error") }) + + t.Run("err with integrations-reserved props", func(t *testing.T) { + + originalHardenedModeSetting := *th.App.Config().ServiceSettings.ExperimentalEnableHardenedMode + th.App.UpdateConfig(func(cfg *model.Config) { + *cfg.ServiceSettings.ExperimentalEnableHardenedMode = true + }) + + defer th.App.UpdateConfig(func(cfg *model.Config) { + *cfg.ServiceSettings.ExperimentalEnableHardenedMode = originalHardenedModeSetting + }) + + post := &model.Post{ + ChannelId: channel.Id, + Message: "#hashtag a message", + CreateAt: model.GetMillis() - 2000, + } + post, _, createErr := th.SystemAdminClient.CreatePost(context.Background(), post) + require.NoError(t, createErr) + + patch := &model.PostPatch{} + patch.Props = &model.StringInterface{model.PostPropsFromWebhook: "true"} + _, patchResp, patchErr := client.PatchPost(context.Background(), post.Id, patch) + + require.Error(t, patchErr) + CheckBadRequestStatus(t, patchResp) + }) } func TestPinPost(t *testing.T) { diff --git a/server/channels/api4/preference_test.go b/server/channels/api4/preference_test.go index 4c2c15673af..5478da7707d 100644 --- a/server/channels/api4/preference_test.go +++ b/server/channels/api4/preference_test.go @@ -50,8 +50,8 @@ func TestGetPreferences(t *testing.T) { prefs, _, err := client.GetPreferences(context.Background(), user1.Id) require.NoError(t, err) - // 5 because we have 2 initial preferences tutorial_step and recommended_next_steps added when creating a new user - require.Equal(t, len(prefs), 5, "received the wrong number of preferences") + // 6 because we have 3 initial preferences tutorial_step, recommended_next_steps and system_notification are added when creating a new user + require.Equal(t, len(prefs), 6, "received the wrong number of preferences") for _, preference := range prefs { require.Equal(t, preference.UserId, th.BasicUser.Id, "user id does not match") diff --git a/server/channels/api4/saml.go b/server/channels/api4/saml.go index 479431274fd..4a2e4a24528 100644 --- a/server/channels/api4/saml.go +++ b/server/channels/api4/saml.go @@ -272,7 +272,7 @@ func resetAuthDataToEmail(c *Context, w http.ResponseWriter, r *http.Request) { } var params *ResetAuthDataParams jsonErr := json.NewDecoder(r.Body).Decode(¶ms) - if jsonErr != nil { + if jsonErr != nil || params == nil { c.Err = model.NewAppError("resetAuthDataToEmail", "model.utils.decode_json.app_error", nil, "", http.StatusBadRequest).Wrap(jsonErr) return } diff --git a/server/channels/api4/system.go b/server/channels/api4/system.go index 273818888cf..2ff8c80e75e 100644 --- a/server/channels/api4/system.go +++ b/server/channels/api4/system.go @@ -26,9 +26,11 @@ import ( ) const ( - RedirectLocationCacheSize = 10000 - DefaultServerBusySeconds = 3600 - MaxServerBusySeconds = 86400 + RedirectLocationCacheSize = 10000 + RedirectLocationMaximumLength = 2100 + RedirectLocationCacheExpiry = 1 * time.Hour + DefaultServerBusySeconds = 3600 + MaxServerBusySeconds = 86400 ) var redirectLocationDataCache = cache.NewLRU(cache.LRUOptions{ @@ -96,7 +98,7 @@ func generateSupportPacket(c *Context, w http.ResponseWriter, r *http.Request) { return } - fileDatas := c.App.GenerateSupportPacket() + fileDatas := c.App.GenerateSupportPacket(c.AppContext) // Constructing the ZIP file name as per spec (mattermost_support_packet_YYYY-MM-DD-HH-MM.zip) now := time.Now() @@ -345,7 +347,7 @@ func queryLogs(c *Context, w http.ResponseWriter, r *http.Request) { var logFilter *model.LogFilter err := json.NewDecoder(r.Body).Decode(&logFilter) - if err != nil { + if err != nil || logFilter == nil { c.Err = model.NewAppError("queryLogs", "api.system.logs.invalidFilter", nil, "", http.StatusInternalServerError) return } @@ -589,7 +591,7 @@ func getRedirectLocation(c *Context, w http.ResponseWriter, r *http.Request) { res, err := client.Head(url) if err != nil { // Cache failures to prevent retries. - redirectLocationDataCache.SetWithExpiry(url, "", 1*time.Hour) + redirectLocationDataCache.SetWithExpiry(url, "", RedirectLocationCacheExpiry) // Always return a success status and a JSON string to limit information returned to client. w.Write([]byte(model.MapToJSON(m))) return @@ -600,7 +602,17 @@ func getRedirectLocation(c *Context, w http.ResponseWriter, r *http.Request) { }() location = res.Header.Get("Location") - redirectLocationDataCache.SetWithExpiry(url, location, 1*time.Hour) + + // If the location length is > 2100, we can probably ignore. Fixes https://mattermost.atlassian.net/browse/MM-54219 + if len(location) > RedirectLocationMaximumLength { + // Treating as a "failure". Cache failures to prevent retries. + redirectLocationDataCache.SetWithExpiry(url, "", RedirectLocationCacheExpiry) + // Always return a success status and a JSON string to limit information returned to client. + w.Write([]byte(model.MapToJSON(m))) + return + } + + redirectLocationDataCache.SetWithExpiry(url, location, RedirectLocationCacheExpiry) m["location"] = location w.Write([]byte(model.MapToJSON(m))) diff --git a/server/channels/api4/system_test.go b/server/channels/api4/system_test.go index 6b3f1911f19..affcdd4e41f 100644 --- a/server/channels/api4/system_test.go +++ b/server/channels/api4/system_test.go @@ -673,6 +673,33 @@ func TestRedirectLocation(t *testing.T) { _, resp, err = client.GetRedirectLocation(context.Background(), "", "") require.Error(t, err) CheckUnauthorizedStatus(t, resp) + + // Check that too-long redirect locations are ignored + *th.App.Config().ServiceSettings.EnableLinkPreviews = true + urlPrefix := "https://example.co" + almostTooLongUrl := urlPrefix + strings.Repeat("a", 2100-len(urlPrefix)) + testServer2 := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) { + res.Header().Set("Location", almostTooLongUrl) + res.WriteHeader(http.StatusFound) + res.Write([]byte("body")) + })) + defer func() { testServer2.Close() }() + + actual, _, err = th.SystemAdminClient.GetRedirectLocation(context.Background(), testServer2.URL, "") + require.NoError(t, err) + assert.Equal(t, almostTooLongUrl, actual) + + tooLongUrl := urlPrefix + strings.Repeat("a", 2101-len(urlPrefix)) + testServer3 := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) { + res.Header().Set("Location", tooLongUrl) + res.WriteHeader(http.StatusFound) + res.Write([]byte("body")) + })) + defer func() { testServer3.Close() }() + + actual, _, err = th.SystemAdminClient.GetRedirectLocation(context.Background(), testServer3.URL, "") + require.NoError(t, err) + assert.Equal(t, "", actual) } func TestSetServerBusy(t *testing.T) { diff --git a/server/channels/api4/team.go b/server/channels/api4/team.go index 87b1a8b98eb..24349b8806d 100644 --- a/server/channels/api4/team.go +++ b/server/channels/api4/team.go @@ -1432,7 +1432,7 @@ func inviteUsersToTeam(c *Context, w http.ResponseWriter, r *http.Request) { } // we then manually schedule the job to send another invite after 48 hours - _, appErr = c.App.Srv().Jobs.CreateJob(model.JobTypeResendInvitationEmail, jobData) + _, appErr = c.App.Srv().Jobs.CreateJob(c.AppContext, model.JobTypeResendInvitationEmail, jobData) if appErr != nil { c.Err = model.NewAppError("Api4.inviteUsersToTeam", appErr.Id, nil, appErr.Error(), appErr.StatusCode) return @@ -1592,7 +1592,7 @@ func invalidateAllEmailInvites(c *Context, w http.ResponseWriter, r *http.Reques auditRec := c.MakeAuditRecord("invalidateAllEmailInvites", audit.Fail) defer c.LogAuditRec(auditRec) - if err := c.App.InvalidateAllEmailInvites(); err != nil { + if err := c.App.InvalidateAllEmailInvites(c.AppContext); err != nil { c.Err = err return } diff --git a/server/channels/api4/team_test.go b/server/channels/api4/team_test.go index 887d6b90000..02bf1aca75d 100644 --- a/server/channels/api4/team_test.go +++ b/server/channels/api4/team_test.go @@ -20,8 +20,8 @@ import ( "github.com/mattermost/mattermost/server/public/model" "github.com/mattermost/mattermost/server/public/plugin/plugintest/mock" "github.com/mattermost/mattermost/server/public/shared/i18n" + "github.com/mattermost/mattermost/server/public/shared/request" "github.com/mattermost/mattermost/server/v8/channels/app" - "github.com/mattermost/mattermost/server/v8/channels/app/request" "github.com/mattermost/mattermost/server/v8/channels/utils/testutils" "github.com/mattermost/mattermost/server/v8/einterfaces/mocks" "github.com/mattermost/mattermost/server/v8/platform/shared/mail" @@ -3196,15 +3196,14 @@ func TestValidateUserPermissionsOnChannels(t *testing.T) { th := Setup(t).InitBasic() defer th.TearDown() - // define user session and context - context := request.NewContext(context.Background(), model.NewId(), model.NewId(), model.NewId(), model.NewId(), model.NewId(), model.Session{}, nil) + ctx := request.EmptyContext(th.TestLogger) t.Run("User WITH permissions on private channel CAN invite members to it", func(t *testing.T) { channelIds := []string{th.BasicChannel.Id, th.BasicPrivateChannel.Id} require.Len(t, channelIds, 2) - channelIds = th.App.ValidateUserPermissionsOnChannels(context, th.BasicUser.Id, channelIds) + channelIds = th.App.ValidateUserPermissionsOnChannels(ctx, th.BasicUser.Id, channelIds) // basicUser has permission onBasicChannel and BasicPrivateChannel so he can invite to both channels require.Len(t, channelIds, 2) @@ -3216,7 +3215,7 @@ func TestValidateUserPermissionsOnChannels(t *testing.T) { require.Len(t, channelIds, 2) - channelIds = th.App.ValidateUserPermissionsOnChannels(context, th.BasicUser.Id, channelIds) + channelIds = th.App.ValidateUserPermissionsOnChannels(ctx, th.BasicUser.Id, channelIds) // basicUser DOES NOT have permission on BasicPrivateChannel2 so he can only invite to BasicChannel require.Len(t, channelIds, 1) diff --git a/server/channels/api4/user.go b/server/channels/api4/user.go index 30204b90769..145070a471d 100644 --- a/server/channels/api4/user.go +++ b/server/channels/api4/user.go @@ -14,6 +14,8 @@ import ( "github.com/mattermost/mattermost/server/public/model" "github.com/mattermost/mattermost/server/public/shared/mlog" + pUtils "github.com/mattermost/mattermost/server/public/utils" + "github.com/mattermost/mattermost/server/v8/channels/app" "github.com/mattermost/mattermost/server/v8/channels/audit" "github.com/mattermost/mattermost/server/v8/channels/store" @@ -61,6 +63,7 @@ func (api *API) InitUser() { api.BaseRoutes.User.Handle("/mfa/generate", api.APISessionRequiredMfa(generateMfaSecret)).Methods("POST") api.BaseRoutes.Users.Handle("/login", api.APIHandler(login)).Methods("POST") + api.BaseRoutes.Users.Handle("/login/desktop_token", api.RateLimitedHandler(api.APIHandler(loginWithDesktopToken), model.RateLimitSettings{PerSec: model.NewInt(2), MaxBurst: model.NewInt(1)})).Methods("POST") api.BaseRoutes.Users.Handle("/login/switch", api.APIHandler(switchAccountType)).Methods("POST") api.BaseRoutes.Users.Handle("/login/cws", api.APIHandlerTrustRequester(loginCWS)).Methods("POST") api.BaseRoutes.Users.Handle("/logout", api.APIHandler(logout)).Methods("POST") @@ -557,12 +560,14 @@ func getFilteredUsersStats(c *Context, w http.ResponseWriter, r *http.Request) { channelID := r.URL.Query().Get("in_channel") includeDeleted := r.URL.Query().Get("include_deleted") includeBotAccounts := r.URL.Query().Get("include_bots") + includeRemoteUsers := r.URL.Query().Get("include_remote_users") rolesString := r.URL.Query().Get("roles") channelRolesString := r.URL.Query().Get("channel_roles") teamRolesString := r.URL.Query().Get("team_roles") includeDeletedBool, _ := strconv.ParseBool(includeDeleted) includeBotAccountsBool, _ := strconv.ParseBool(includeBotAccounts) + includeRemoteUsersBool, _ := strconv.ParseBool(includeRemoteUsers) roles := []string{} var rolesValid bool @@ -593,6 +598,7 @@ func getFilteredUsersStats(c *Context, w http.ResponseWriter, r *http.Request) { options := &model.UserCountOptions{ IncludeDeleted: includeDeletedBool, IncludeBotAccounts: includeBotAccountsBool, + IncludeRemoteUsers: includeRemoteUsersBool, TeamId: teamID, ChannelId: channelID, Roles: roles, @@ -718,7 +724,7 @@ func getUsers(c *Context, w http.ResponseWriter, r *http.Request) { c.SetInvalidParam("role") return } - roleValid := utils.StringInSlice(role, roleNamesAll) + roleValid := pUtils.Contains(roleNamesAll, role) if !roleValid { c.SetInvalidParam("role") return @@ -954,6 +960,10 @@ func getUsersByIds(c *Context, w http.ResponseWriter, r *http.Request) { return } + // we remove the duplicate IDs as it can bring a significant load to the + // database. + userIDs = model.RemoveDuplicateStrings(userIDs) + sinceString := r.URL.Query().Get("since") options := &store.UserGetByIdsOpts{ @@ -1832,6 +1842,7 @@ func login(c *Context, w http.ResponseWriter, r *http.Request) { "api.user.check_user_mfa.bad_code.app_error", "api.user.login.blank_pwd.app_error", "api.user.login.bot_login_forbidden.app_error", + "api.user.login.remote_users.login.error", "api.user.login.client_side_cert.certificate.app_error", "api.user.login.inactive.app_error", "api.user.login.not_verified.app_error", @@ -1932,9 +1943,15 @@ func login(c *Context, w http.ResponseWriter, r *http.Request) { } } + if user.IsRemote() { + c.Err = model.NewAppError("login", "api.user.login.remote_users.login.error", nil, "", http.StatusUnauthorized) + return + } + c.LogAuditWithUserId(user.Id, "authenticated") - err = c.App.DoLogin(c.AppContext, w, r, user, deviceId, false, false, false) + isMobileDevice := utils.IsMobileRequest(r) + err = c.App.DoLogin(c.AppContext, w, r, user, deviceId, isMobileDevice, false, false) if err != nil { c.Err = err return @@ -1965,6 +1982,30 @@ func login(c *Context, w http.ResponseWriter, r *http.Request) { } } +func loginWithDesktopToken(c *Context, w http.ResponseWriter, r *http.Request) { + props := model.MapFromJSON(r.Body) + token := props["token"] + deviceId := props["device_id"] + + user, err := c.App.ValidateDesktopToken(token, time.Now().Add(-model.DesktopTokenTTL).Unix()) + if err != nil { + c.Err = err + return + } + + err = c.App.DoLogin(c.AppContext, w, r, user, deviceId, false, false, false) + if err != nil { + c.Err = err + return + } + + c.App.AttachSessionCookies(c.AppContext, w, r) + + if err := json.NewEncoder(w).Encode(user); err != nil { + c.Logger.Warn("Error while writing response", mlog.Err(err)) + } +} + func loginCWS(c *Context, w http.ResponseWriter, r *http.Request) { campaignToURL := map[string]string{ "focalboard": "/boards", @@ -2004,7 +2045,8 @@ func loginCWS(c *Context, w http.ResponseWriter, r *http.Request) { } audit.AddEventParameterAuditable(auditRec, "user", user) c.LogAuditWithUserId(user.Id, "authenticated") - err = c.App.DoLogin(c.AppContext, w, r, user, "", false, false, false) + isMobileDevice := utils.IsMobileRequest(r) + err = c.App.DoLogin(c.AppContext, w, r, user, "", isMobileDevice, false, false) if err != nil { c.LogErrorByCode(err) http.Redirect(w, r, *c.App.Config().ServiceSettings.SiteURL, http.StatusFound) @@ -3030,7 +3072,7 @@ func migrateAuthToLDAP(c *Context, w http.ResponseWriter, r *http.Request) { } if migrate := c.App.AccountMigration(); migrate != nil { - if err := migrate.MigrateToLdap(from, matchField, force, false); err != nil { + if err := migrate.MigrateToLdap(c.AppContext, from, matchField, force, false); err != nil { c.Err = model.NewAppError("api.migrateAuthToLdap", "api.migrate_to_saml.error", nil, err.Error(), http.StatusInternalServerError) return } diff --git a/server/channels/api4/user_local.go b/server/channels/api4/user_local.go index f4d423d728c..2171d14f7cd 100644 --- a/server/channels/api4/user_local.go +++ b/server/channels/api4/user_local.go @@ -11,6 +11,8 @@ import ( "github.com/mattermost/mattermost/server/public/model" "github.com/mattermost/mattermost/server/public/shared/mlog" + pUtils "github.com/mattermost/mattermost/server/public/utils" + "github.com/mattermost/mattermost/server/v8/channels/audit" "github.com/mattermost/mattermost/server/v8/channels/store" "github.com/mattermost/mattermost/server/v8/channels/utils" @@ -85,7 +87,7 @@ func localGetUsers(c *Context, w http.ResponseWriter, r *http.Request) { c.SetInvalidParam("role") return } - roleValid := utils.StringInSlice(role, roleNamesAll) + roleValid := pUtils.Contains(roleNamesAll, role) if !roleValid { c.SetInvalidParam("role") return @@ -231,13 +233,17 @@ func localGetUsers(c *Context, w http.ResponseWriter, r *http.Request) { } func localGetUsersByIds(c *Context, w http.ResponseWriter, r *http.Request) { - userIds := model.ArrayFromJSON(r.Body) + userIDs := model.ArrayFromJSON(r.Body) - if len(userIds) == 0 { + if len(userIDs) == 0 { c.SetInvalidParam("user_ids") return } + // we remove the duplicate IDs as it can bring a significant load to the + // database. + userIDs = model.RemoveDuplicateStrings(userIDs) + sinceString := r.URL.Query().Get("since") options := &store.UserGetByIdsOpts{ @@ -253,7 +259,7 @@ func localGetUsersByIds(c *Context, w http.ResponseWriter, r *http.Request) { options.Since = since } - users, appErr := c.App.GetUsersByIds(userIds, options) + users, appErr := c.App.GetUsersByIds(userIDs, options) if appErr != nil { c.Err = appErr return diff --git a/server/channels/api4/user_test.go b/server/channels/api4/user_test.go index f3be8485dc4..e2e65cf7acc 100644 --- a/server/channels/api4/user_test.go +++ b/server/channels/api4/user_test.go @@ -1725,6 +1725,13 @@ func TestGetUsersByIds(t *testing.T) { require.Len(t, users, 1, "1 user should be returned") }) + + t.Run("should only return unique users when multiple IDs are requested", func(t *testing.T) { + users, _, err := client.GetUsersByIds(context.Background(), []string{th.BasicUser.Id, th.BasicUser.Id, th.BasicUser.Id}) + require.NoError(t, err) + + require.Len(t, users, 1, "1 user should be returned") + }) }) t.Run("should return error when not logged in", func(t *testing.T) { @@ -3842,6 +3849,18 @@ func TestLogin(t *testing.T) { CheckErrorID(t, err, "api.user.login.bot_login_forbidden.app_error") }) + t.Run("remote user login rejected", func(t *testing.T) { + email := th.GenerateTestEmail() + user := model.User{Email: email, Nickname: "Darth Vader", Password: "hello1", Username: GenerateTestUsername(), Roles: model.SystemAdminRoleId + " " + model.SystemUserRoleId, RemoteId: model.NewString("remote-id")} + ruser, _, _ := th.Client.CreateUser(context.Background(), &user) + + _, err := th.SystemAdminClient.UpdateUserPassword(context.Background(), ruser.Id, "", "password") + require.NoError(t, err) + + _, _, err = th.Client.Login(context.Background(), ruser.Email, "password") + CheckErrorID(t, err, "api.user.login.remote_users.login.error") + }) + t.Run("login with terms_of_service set", func(t *testing.T) { termsOfService, appErr := th.App.CreateTermsOfService("terms of service", th.BasicUser.Id) require.Nil(t, appErr) diff --git a/server/channels/app/admin.go b/server/channels/app/admin.go index 7e1678cafcf..95afdff8a6a 100644 --- a/server/channels/app/admin.go +++ b/server/channels/app/admin.go @@ -12,7 +12,7 @@ import ( "github.com/mattermost/mattermost/server/public/model" "github.com/mattermost/mattermost/server/public/shared/i18n" - "github.com/mattermost/mattermost/server/v8/channels/app/request" + "github.com/mattermost/mattermost/server/public/shared/request" "github.com/mattermost/mattermost/server/v8/platform/services/cache" "github.com/mattermost/mattermost/server/v8/platform/shared/mail" ) diff --git a/server/channels/app/admin_advisor.go b/server/channels/app/admin_advisor.go index c68279ba12d..27c90dea2f2 100644 --- a/server/channels/app/admin_advisor.go +++ b/server/channels/app/admin_advisor.go @@ -10,7 +10,7 @@ import ( "github.com/mattermost/mattermost/server/public/model" "github.com/mattermost/mattermost/server/public/shared/i18n" "github.com/mattermost/mattermost/server/public/shared/mlog" - "github.com/mattermost/mattermost/server/v8/channels/app/request" + "github.com/mattermost/mattermost/server/public/shared/request" "github.com/mattermost/mattermost/server/v8/platform/shared/mail" ) diff --git a/server/channels/app/app_iface.go b/server/channels/app/app_iface.go index 720e8aed524..4bffa50df6e 100644 --- a/server/channels/app/app_iface.go +++ b/server/channels/app/app_iface.go @@ -22,9 +22,9 @@ import ( "github.com/mattermost/mattermost/server/public/plugin" "github.com/mattermost/mattermost/server/public/shared/i18n" "github.com/mattermost/mattermost/server/public/shared/mlog" + "github.com/mattermost/mattermost/server/public/shared/request" "github.com/mattermost/mattermost/server/public/shared/timezones" "github.com/mattermost/mattermost/server/v8/channels/app/platform" - "github.com/mattermost/mattermost/server/v8/channels/app/request" "github.com/mattermost/mattermost/server/v8/channels/audit" "github.com/mattermost/mattermost/server/v8/channels/product" "github.com/mattermost/mattermost/server/v8/channels/store" @@ -333,7 +333,7 @@ type AppIface interface { // SyncLdap starts an LDAP sync job. // If includeRemovedMembers is true, then members who left or were removed from a team/channel will // be re-added; otherwise, they will not be re-added. - SyncLdap(includeRemovedMembers bool) + SyncLdap(c *request.Context, includeRemovedMembers bool) // SyncPlugins synchronizes the plugins installed locally // with the plugin bundles available in the file store. SyncPlugins() *model.AppError @@ -444,7 +444,7 @@ type AppIface interface { BulkImport(c *request.Context, jsonlReader io.Reader, attachmentsReader *zip.Reader, dryRun bool, workers int) (*model.AppError, int) BulkImportWithPath(c *request.Context, jsonlReader io.Reader, attachmentsReader *zip.Reader, dryRun bool, workers int, importPath string) (*model.AppError, int) CanNotifyAdmin(trial bool) bool - CancelJob(jobId string) *model.AppError + CancelJob(c *request.Context, jobId string) *model.AppError ChannelMembersToRemove(teamID *string) ([]*model.ChannelMember, *model.AppError) Channels() *Channels CheckCanInviteToSharedChannel(channelId string) error @@ -477,6 +477,7 @@ type AppIface interface { CompleteSwitchWithOAuth(service string, userData io.Reader, email string, tokenUser *model.User) (*model.User, *model.AppError) Compliance() einterfaces.ComplianceInterface Config() *model.Config + ConvertGroupMessageToChannel(c request.CTX, convertedByUserId string, gmConversionRequest *model.GroupMessageConversionRequestBody) (*model.Channel, *model.AppError) CopyFileInfos(userID string, fileIDs []string) ([]string, *model.AppError) CreateChannel(c request.CTX, channel *model.Channel, addMember bool) (*model.Channel, *model.AppError) CreateChannelWithUser(c request.CTX, channel *model.Channel, userID string) (*model.Channel, *model.AppError) @@ -487,7 +488,7 @@ type AppIface interface { CreateGroupChannel(c request.CTX, userIDs []string, creatorId string) (*model.Channel, *model.AppError) CreateGroupWithUserIds(group *model.GroupWithUserIds) (*model.Group, *model.AppError) CreateIncomingWebhookForChannel(creatorId string, channel *model.Channel, hook *model.IncomingWebhook) (*model.IncomingWebhook, *model.AppError) - CreateJob(job *model.Job) (*model.Job, *model.AppError) + CreateJob(c *request.Context, job *model.Job) (*model.Job, *model.AppError) CreateOAuthApp(app *model.OAuthApp) (*model.OAuthApp, *model.AppError) CreateOAuthStateToken(extra string) (*model.Token, *model.AppError) CreateOAuthUser(c *request.Context, service string, userData io.Reader, teamID string, tokenUser *model.User) (*model.User, *model.AppError) @@ -577,10 +578,11 @@ type AppIface interface { FilterUsersByVisible(viewer *model.User, otherUsers []*model.User) ([]*model.User, *model.AppError) FindTeamByName(name string) bool FinishSendAdminNotifyPost(trial bool, now int64, pluginBasedData map[string][]*model.NotifyAdminData) + GenerateAndSaveDesktopToken(createAt int64, user *model.User) (*string, *model.AppError) GenerateMfaSecret(userID string) (*model.MfaSecret, *model.AppError) GeneratePresignURLForExport(name string) (*model.PresignURLResponse, *model.AppError) GeneratePublicLink(siteURL string, info *model.FileInfo) string - GenerateSupportPacket() []model.FileData + GenerateSupportPacket(c *request.Context) []model.FileData GetAcknowledgementsForPost(postID string) ([]*model.PostAcknowledgement, *model.AppError) GetAcknowledgementsForPostList(postList *model.PostList) (map[string][]*model.PostAcknowledgement, *model.AppError) GetActivePluginManifests() ([]*model.Manifest, *model.AppError) @@ -663,6 +665,7 @@ type AppIface interface { GetGroupMemberUsers(groupID string) ([]*model.User, *model.AppError) GetGroupMemberUsersPage(groupID string, page int, perPage int, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, int, *model.AppError) GetGroupMemberUsersSortedPage(groupID string, page int, perPage int, viewRestrictions *model.ViewUsersRestrictions, teammateNameDisplay string) ([]*model.User, int, *model.AppError) + GetGroupMessageMembersCommonTeams(c request.CTX, channelID string) ([]*model.Team, *model.AppError) GetGroupSyncable(groupID string, syncableID string, syncableType model.GroupSyncableType) (*model.GroupSyncable, *model.AppError) GetGroupSyncables(groupID string, syncableType model.GroupSyncableType) ([]*model.GroupSyncable, *model.AppError) GetGroups(page, perPage int, opts model.GroupSearchOpts, viewRestrictions *model.ViewUsersRestrictions) ([]*model.Group, *model.AppError) @@ -677,13 +680,11 @@ type AppIface interface { GetIncomingWebhooksForTeamPageByUser(teamID string, userID string, page, perPage int) ([]*model.IncomingWebhook, *model.AppError) GetIncomingWebhooksPage(page, perPage int) ([]*model.IncomingWebhook, *model.AppError) GetIncomingWebhooksPageByUser(userID string, page, perPage int) ([]*model.IncomingWebhook, *model.AppError) - GetJob(id string) (*model.Job, *model.AppError) - GetJobs(offset int, limit int) ([]*model.Job, *model.AppError) - GetJobsByType(jobType string, offset int, limit int) ([]*model.Job, *model.AppError) - GetJobsByTypePage(jobType string, page int, perPage int) ([]*model.Job, *model.AppError) - GetJobsByTypes(jobTypes []string, offset int, limit int) ([]*model.Job, *model.AppError) - GetJobsByTypesPage(jobType []string, page int, perPage int) ([]*model.Job, *model.AppError) - GetJobsPage(page int, perPage int) ([]*model.Job, *model.AppError) + GetJob(c *request.Context, id string) (*model.Job, *model.AppError) + GetJobsByType(c *request.Context, jobType string, offset int, limit int) ([]*model.Job, *model.AppError) + GetJobsByTypePage(c *request.Context, jobType string, page int, perPage int) ([]*model.Job, *model.AppError) + GetJobsByTypes(c *request.Context, jobTypes []string, offset int, limit int) ([]*model.Job, *model.AppError) + GetJobsByTypesPage(c *request.Context, jobType []string, page int, perPage int) ([]*model.Job, *model.AppError) GetLatestTermsOfService() (*model.TermsOfService, *model.AppError) GetLatestVersion(latestVersionUrl string) (*model.GithubReleaseInfo, *model.AppError) GetLogs(c request.CTX, page, perPage int) ([]string, *model.AppError) @@ -702,8 +703,8 @@ type AppIface interface { GetOAuthAppsByCreator(userID string, page, perPage int) ([]*model.OAuthApp, *model.AppError) GetOAuthCodeRedirect(userID string, authRequest *model.AuthorizeRequest) (string, *model.AppError) GetOAuthImplicitRedirect(userID string, authRequest *model.AuthorizeRequest) (string, *model.AppError) - GetOAuthLoginEndpoint(w http.ResponseWriter, r *http.Request, service, teamID, action, redirectTo, loginHint string, isMobile bool) (string, *model.AppError) - GetOAuthSignupEndpoint(w http.ResponseWriter, r *http.Request, service, teamID string) (string, *model.AppError) + GetOAuthLoginEndpoint(w http.ResponseWriter, r *http.Request, service, teamID, action, redirectTo, loginHint string, isMobile bool, desktopToken string) (string, *model.AppError) + GetOAuthSignupEndpoint(w http.ResponseWriter, r *http.Request, service, teamID string, desktopToken string) (string, *model.AppError) GetOAuthStateToken(token string) (*model.Token, *model.AppError) GetOnboarding() (*model.System, *model.AppError) GetOpenGraphMetadata(requestURL string) ([]byte, error) @@ -820,6 +821,7 @@ type AppIface interface { GetUserAccessTokensForUser(userID string, page, perPage int) ([]*model.UserAccessToken, *model.AppError) GetUserByAuth(authData *string, authService string) (*model.User, *model.AppError) GetUserByEmail(email string) (*model.User, *model.AppError) + GetUserByRemoteID(remoteID string) (*model.User, *model.AppError) GetUserByUsername(username string) (*model.User, *model.AppError) GetUserForLogin(id, loginId string) (*model.User, *model.AppError) GetUserTermsOfService(userID string) (*model.UserTermsOfService, *model.AppError) @@ -875,8 +877,8 @@ type AppIface interface { ImageProxyRemover() (f func(string) string) ImportPermissions(jsonl io.Reader) error InitPlugins(c *request.Context, pluginDir, webappPluginDir string) - InvalidateAllEmailInvites() *model.AppError - InvalidateAllResendInviteEmailJobs() *model.AppError + InvalidateAllEmailInvites(c *request.Context) *model.AppError + InvalidateAllResendInviteEmailJobs(c *request.Context) *model.AppError InvalidateCacheForUser(userID string) InvalidatePasswordRecoveryTokensForUser(userID string) *model.AppError InviteGuestsToChannels(teamID string, guestsInvite *model.GuestsInvite, senderId string) *model.AppError @@ -1067,6 +1069,7 @@ type AppIface interface { SetChannels(ch *Channels) SetCustomStatus(c request.CTX, userID string, cs *model.CustomStatus) *model.AppError SetDefaultProfileImage(c request.CTX, user *model.User) *model.AppError + SetFileSearchableContent(fileID string, data string) *model.AppError SetPhase2PermissionsMigrationStatus(isComplete bool) error SetPluginKey(pluginID string, key string, value []byte) *model.AppError SetPluginKeyWithExpiry(pluginID string, key string, value []byte, expireInSeconds int64) *model.AppError @@ -1132,7 +1135,7 @@ type AppIface interface { UpdatePasswordAsUser(c request.CTX, userID, currentPassword, newPassword string) *model.AppError UpdatePasswordByUserIdSendEmail(c request.CTX, userID, newPassword, method string) *model.AppError UpdatePasswordSendEmail(c request.CTX, user *model.User, newPassword, method string) *model.AppError - UpdatePost(c *request.Context, post *model.Post, safeUpdate bool) (*model.Post, *model.AppError) + UpdatePost(c *request.Context, receivedUpdatedPost *model.Post, safeUpdate bool) (*model.Post, *model.AppError) UpdatePreferences(userID string, preferences model.Preferences) *model.AppError UpdateRemoteCluster(rc *model.RemoteCluster) (*model.RemoteCluster, *model.AppError) UpdateRemoteClusterTopics(remoteClusterId string, topics string) (*model.RemoteCluster, *model.AppError) @@ -1160,6 +1163,7 @@ type AppIface interface { UpdateUserRolesWithUser(c request.CTX, user *model.User, newRoles string, sendWebSocketEvent bool) (*model.User, *model.AppError) UploadData(c request.CTX, us *model.UploadSession, rd io.Reader) (*model.FileInfo, *model.AppError) UploadEmojiImage(c request.CTX, id string, imageData *multipart.FileHeader) *model.AppError + UploadFileForUserAndTeam(c request.CTX, data []byte, channelID string, filename string, rawUserId string, rawTeamId string) (*model.FileInfo, *model.AppError) UpsertDraft(c *request.Context, draft *model.Draft, connectionID string) (*model.Draft, *model.AppError) UpsertGroupMember(groupID string, userID string) (*model.GroupMember, *model.AppError) UpsertGroupMembers(groupID string, userIDs []string) ([]*model.GroupMember, *model.AppError) @@ -1167,6 +1171,7 @@ type AppIface interface { UserAlreadyNotifiedOnRequiredFeature(user string, feature model.MattermostFeature) bool UserCanSeeOtherUser(userID string, otherUserId string) (bool, *model.AppError) UserIsFirstAdmin(user *model.User) bool + ValidateDesktopToken(token string, expiryTime int64) (*model.User, *model.AppError) VerifyEmailFromToken(c request.CTX, userSuppliedTokenString string) *model.AppError VerifyUserEmail(userID, email string) *model.AppError ViewChannel(c request.CTX, view *model.ChannelView, userID string, currentSessionId string, collapsedThreadsSupported bool) (map[string]int64, *model.AppError) diff --git a/server/channels/app/audit.go b/server/channels/app/audit.go index 200e9d8b690..3a847a1cf67 100644 --- a/server/channels/app/audit.go +++ b/server/channels/app/audit.go @@ -89,10 +89,11 @@ func (a *App) MakeAuditRecord(event string, initialStatus string) *audit.Record audit.KeyClusterID: a.GetClusterId(), }, Actor: audit.EventActor{ - UserId: userID, - SessionId: "", - Client: fmt.Sprintf("server %s-%s", model.BuildNumber, model.BuildHash), - IpAddress: "", + UserId: userID, + SessionId: "", + Client: fmt.Sprintf("server %s-%s", model.BuildNumber, model.BuildHash), + IpAddress: "", + XForwardedFor: "", }, EventData: audit.EventData{ Parameters: map[string]interface{}{}, diff --git a/server/channels/app/authentication.go b/server/channels/app/authentication.go index c2c9d488905..ab63c987ba0 100644 --- a/server/channels/app/authentication.go +++ b/server/channels/app/authentication.go @@ -9,7 +9,7 @@ import ( "strings" "github.com/mattermost/mattermost/server/public/model" - "github.com/mattermost/mattermost/server/v8/channels/app/request" + "github.com/mattermost/mattermost/server/public/shared/request" "github.com/mattermost/mattermost/server/v8/channels/app/users" "github.com/mattermost/mattermost/server/v8/platform/shared/mfa" ) diff --git a/server/channels/app/authorization.go b/server/channels/app/authorization.go index de20eb869f3..82f1674e21c 100644 --- a/server/channels/app/authorization.go +++ b/server/channels/app/authorization.go @@ -11,7 +11,7 @@ import ( "github.com/mattermost/mattermost/server/public/model" "github.com/mattermost/mattermost/server/public/shared/mlog" - "github.com/mattermost/mattermost/server/v8/channels/app/request" + "github.com/mattermost/mattermost/server/public/shared/request" ) func (a *App) MakePermissionError(s *model.Session, permissions []*model.Permission) *model.AppError { @@ -263,7 +263,7 @@ func (a *App) SessionHasPermissionToUserOrBot(session model.Session, userID stri if err == nil { return true } - if err.Id == "store.sql_bot.get.missing.app_error" && err.Unwrap() != nil { + if err.Id == "store.sql_bot.get.missing.app_error" && err.Where == "SqlBotStore.Get" { if a.SessionHasPermissionToUser(session, userID) { return true } @@ -385,7 +385,7 @@ func (a *App) SessionHasPermissionToManageBot(session model.Session, botUserId s if !a.SessionHasPermissionTo(session, model.PermissionReadBots) { // If the user doesn't have permission to read bots, pretend as if // the bot doesn't exist at all. - return model.MakeBotNotFoundError(botUserId) + return model.MakeBotNotFoundError("permissions", botUserId) } return a.MakePermissionError(&session, []*model.Permission{model.PermissionManageBots}) } @@ -394,7 +394,7 @@ func (a *App) SessionHasPermissionToManageBot(session model.Session, botUserId s if !a.SessionHasPermissionTo(session, model.PermissionReadOthersBots) { // If the user doesn't have permission to read others' bots, // pretend as if the bot doesn't exist at all. - return model.MakeBotNotFoundError(botUserId) + return model.MakeBotNotFoundError("permissions", botUserId) } return a.MakePermissionError(&session, []*model.Permission{model.PermissionManageOthersBots}) } @@ -404,5 +404,8 @@ func (a *App) SessionHasPermissionToManageBot(session model.Session, botUserId s } func (a *App) HasPermissionToReadChannel(c request.CTX, userID string, channel *model.Channel) bool { + if !*a.Config().TeamSettings.ExperimentalViewArchivedChannels && channel.DeleteAt != 0 { + return false + } return a.HasPermissionToChannel(c, userID, channel.Id, model.PermissionReadChannelContent) || (channel.Type == model.ChannelTypeOpen && a.HasPermissionToTeam(userID, channel.TeamId, model.PermissionReadPublicChannel)) } diff --git a/server/channels/app/authorization_test.go b/server/channels/app/authorization_test.go index 7ed67118a55..19214728142 100644 --- a/server/channels/app/authorization_test.go +++ b/server/channels/app/authorization_test.go @@ -185,6 +185,32 @@ func TestSessionHasPermissionToManageBot(t *testing.T) { th.RemovePermissionFromRole(model.PermissionManageOthersBots.Id, model.SystemUserRoleId) }) + t.Run("test user manager access", func(t *testing.T) { + session := model.Session{ + UserId: th.BasicUser2.Id, + Roles: model.SystemUserManagerRoleId, + } + + // test non bot, contains wrapped error + err = th.App.SessionHasPermissionToManageBot(session, "12345") + assert.NotNil(t, err) + assert.Equal(t, "store.sql_bot.get.missing.app_error", err.Id) + assert.Error(t, err.Unwrap()) + + // test existing bot, without PermissionManageOthersBots - no wrapped error + err = th.App.SessionHasPermissionToManageBot(session, bot.UserId) + assert.NotNil(t, err) + assert.Equal(t, "store.sql_bot.get.missing.app_error", err.Id) + assert.NoError(t, err.Unwrap()) + + // test with correct permissions + th.AddPermissionToRole(model.PermissionManageOthersBots.Id, model.SystemUserManagerRoleId) + err = th.App.SessionHasPermissionToManageBot(session, bot.UserId) + assert.Nil(t, err) + + th.RemovePermissionFromRole(model.PermissionManageOthersBots.Id, model.SystemUserManagerRoleId) + }) + t.Run("test sysadmin role", func(t *testing.T) { session := model.Session{ UserId: th.SystemAdminUser.Id, @@ -229,6 +255,17 @@ func TestSessionHasPermissionToUser(t *testing.T) { th.AddPermissionToRole(model.PermissionEditOtherUsers.Id, model.SystemUserManagerRoleId) assert.True(t, th.App.SessionHasPermissionToUser(session, th.BasicUser2.Id)) th.RemovePermissionFromRole(model.PermissionEditOtherUsers.Id, model.SystemUserManagerRoleId) + + bot, err := th.App.CreateBot(th.Context, &model.Bot{ + Username: "username", + Description: "a bot", + OwnerId: th.BasicUser2.Id, + }) + require.Nil(t, err) + assert.NotNil(t, bot) + defer th.App.PermanentDeleteBot(bot.UserId) + + assert.False(t, th.App.SessionHasPermissionToUser(session, bot.UserId)) }) t.Run("test admin user access", func(t *testing.T) { diff --git a/server/channels/app/auto_responder.go b/server/channels/app/auto_responder.go index c9a66c333d6..55b80c19e84 100644 --- a/server/channels/app/auto_responder.go +++ b/server/channels/app/auto_responder.go @@ -8,7 +8,7 @@ import ( "time" "github.com/mattermost/mattermost/server/public/model" - "github.com/mattermost/mattermost/server/v8/channels/app/request" + "github.com/mattermost/mattermost/server/public/shared/request" ) // check if there is any auto_response type post in channel by the user in a calender day diff --git a/server/channels/app/bot.go b/server/channels/app/bot.go index 8ac5e6a414c..8fdd4c32678 100644 --- a/server/channels/app/bot.go +++ b/server/channels/app/bot.go @@ -12,7 +12,8 @@ import ( "github.com/mattermost/mattermost/server/public/model" "github.com/mattermost/mattermost/server/public/shared/i18n" "github.com/mattermost/mattermost/server/public/shared/mlog" - "github.com/mattermost/mattermost/server/v8/channels/app/request" + + "github.com/mattermost/mattermost/server/public/shared/request" "github.com/mattermost/mattermost/server/v8/channels/product" "github.com/mattermost/mattermost/server/v8/channels/store" ) @@ -153,11 +154,7 @@ func (a *App) CreateBot(c request.CTX, bot *model.Bot) (*model.Bot, *model.AppEr } else if ownerUser != nil { // Send a message to the bot's creator to inform them that the bot needs to be added // to a team and channel after it's created - botOwner, err := a.GetUser(bot.OwnerId) - if err != nil { - return nil, err - } - channel, err := a.getOrCreateDirectChannelWithUser(c, user, botOwner) + channel, err := a.getOrCreateDirectChannelWithUser(c, user, ownerUser) if err != nil { return nil, err } @@ -356,7 +353,7 @@ func (a *App) PatchBot(botUserId string, botPatch *model.BotPatch) (*model.Bot, var appErr *model.AppError switch { case errors.As(nErr, &nfErr): - return nil, model.MakeBotNotFoundError(nfErr.ID).Wrap(nErr) + return nil, model.MakeBotNotFoundError("SqlBotStore.Get", nfErr.ID).Wrap(nErr) case errors.As(nErr, &appErr): // in case we haven't converted to plain error. return nil, appErr default: // last fallback in case it doesn't map to an existing app error. @@ -373,7 +370,7 @@ func (a *App) GetBot(botUserId string, includeDeleted bool) (*model.Bot, *model. var nfErr *store.ErrNotFound switch { case errors.As(err, &nfErr): - return nil, model.MakeBotNotFoundError(nfErr.ID).Wrap(err) + return nil, model.MakeBotNotFoundError("SqlBotStore.Get", nfErr.ID).Wrap(err) default: // last fallback in case it doesn't map to an existing app error. return nil, model.NewAppError("GetBot", "app.bot.getbot.internal_error", nil, "", http.StatusInternalServerError).Wrap(err) } @@ -412,7 +409,7 @@ func (a *App) UpdateBotActive(c request.CTX, botUserId string, active bool) (*mo var nfErr *store.ErrNotFound switch { case errors.As(nErr, &nfErr): - return nil, model.MakeBotNotFoundError(nfErr.ID).Wrap(nErr) + return nil, model.MakeBotNotFoundError("SqlBotStore.Get", nfErr.ID).Wrap(nErr) default: // last fallback in case it doesn't map to an existing app error. return nil, model.NewAppError("UpdateBotActive", "app.bot.getbot.internal_error", nil, "", http.StatusInternalServerError).Wrap(nErr) } @@ -434,7 +431,7 @@ func (a *App) UpdateBotActive(c request.CTX, botUserId string, active bool) (*mo var appErr *model.AppError switch { case errors.As(nErr, &nfErr): - return nil, model.MakeBotNotFoundError(nfErr.ID).Wrap(nErr) + return nil, model.MakeBotNotFoundError("SqlBotStore.Get", nfErr.ID).Wrap(nErr) case errors.As(nErr, &appErr): // in case we haven't converted to plain error. return nil, appErr default: // last fallback in case it doesn't map to an existing app error. @@ -472,7 +469,7 @@ func (a *App) UpdateBotOwner(botUserId, newOwnerId string) (*model.Bot, *model.A var nfErr *store.ErrNotFound switch { case errors.As(err, &nfErr): - return nil, model.MakeBotNotFoundError(nfErr.ID).Wrap(err) + return nil, model.MakeBotNotFoundError("SqlBotStore.Get", nfErr.ID).Wrap(err) default: // last fallback in case it doesn't map to an existing app error. return nil, model.NewAppError("UpdateBotOwner", "app.bot.getbot.internal_error", nil, "", http.StatusInternalServerError).Wrap(err) } @@ -486,7 +483,7 @@ func (a *App) UpdateBotOwner(botUserId, newOwnerId string) (*model.Bot, *model.A var appErr *model.AppError switch { case errors.As(err, &nfErr): - return nil, model.MakeBotNotFoundError(nfErr.ID).Wrap(err) + return nil, model.MakeBotNotFoundError("SqlBotStore.Get", nfErr.ID).Wrap(err) case errors.As(err, &appErr): // in case we haven't converted to plain error. return nil, appErr default: // last fallback in case it doesn't map to an existing app error. diff --git a/server/channels/app/channel.go b/server/channels/app/channel.go index e8def4b3050..2a5b05bae8d 100644 --- a/server/channels/app/channel.go +++ b/server/channels/app/channel.go @@ -11,15 +11,17 @@ import ( "net/http" "strings" + "github.com/mattermost/mattermost/server/v8/channels/utils" + "github.com/mattermost/mattermost/server/public/model" "github.com/mattermost/mattermost/server/public/plugin" "github.com/mattermost/mattermost/server/public/shared/i18n" "github.com/mattermost/mattermost/server/public/shared/mlog" - "github.com/mattermost/mattermost/server/v8/channels/app/request" + "github.com/mattermost/mattermost/server/public/shared/request" + pUtils "github.com/mattermost/mattermost/server/public/utils" "github.com/mattermost/mattermost/server/v8/channels/product" "github.com/mattermost/mattermost/server/v8/channels/store" "github.com/mattermost/mattermost/server/v8/channels/store/sqlstore" - "github.com/mattermost/mattermost/server/v8/channels/utils" ) // channelsWrapper provides an implementation of `product.ChannelService` to be used by products. @@ -1103,7 +1105,7 @@ func (a *App) PatchChannelModerationsForChannel(c request.CTX, channel *model.Ch for _, channelModerationPatch := range channelModerationsPatch { permissionModified := *channelModerationPatch.Name - if channelModerationPatch.Roles.Guests != nil && utils.StringInSlice(permissionModified, model.ChannelModeratedPermissionsChangedByPatch(guestRole, guestRolePatch)) { + if channelModerationPatch.Roles.Guests != nil && pUtils.Contains(model.ChannelModeratedPermissionsChangedByPatch(guestRole, guestRolePatch), permissionModified) { if *channelModerationPatch.Roles.Guests { c.Logger().Info("Permission enabled for guests.", mlog.String("permission", permissionModified), mlog.String("channel_id", channel.Id), mlog.String("channel_name", channel.Name)) } else { @@ -1111,7 +1113,7 @@ func (a *App) PatchChannelModerationsForChannel(c request.CTX, channel *model.Ch } } - if channelModerationPatch.Roles.Members != nil && utils.StringInSlice(permissionModified, model.ChannelModeratedPermissionsChangedByPatch(memberRole, memberRolePatch)) { + if channelModerationPatch.Roles.Members != nil && pUtils.Contains(model.ChannelModeratedPermissionsChangedByPatch(memberRole, memberRolePatch), permissionModified) { if *channelModerationPatch.Roles.Members { c.Logger().Info("Permission enabled for members.", mlog.String("permission", permissionModified), mlog.String("channel_id", channel.Id), mlog.String("channel_name", channel.Name)) } else { @@ -2970,57 +2972,32 @@ func (a *App) SearchChannelsUserNotIn(c request.CTX, teamID string, userID strin } func (a *App) MarkChannelsAsViewed(c request.CTX, channelIDs []string, userID string, currentSessionId string, collapsedThreadsSupported, isCRTEnabled bool) (map[string]int64, *model.AppError) { - // I start looking for channels with notifications before I mark it as read, to clear the push notifications if needed - channelsToClearPushNotifications := []string{} - if a.canSendPushNotifications() { - for _, channelID := range channelIDs { - channel, errCh := a.Srv().Store().Channel().Get(channelID, true) - if errCh != nil { - c.Logger().Warn("Failed to get channel", mlog.Err(errCh)) - continue - } + var err error - member, err := a.Srv().Store().Channel().GetMember(context.Background(), channelID, userID) - if err != nil { - c.Logger().Warn("Failed to get membership", mlog.Err(err)) - continue - } + user, err := a.Srv().Store().User().Get(c.Context(), userID) + if err != nil { + return nil, model.NewAppError("MarkChannelsAsViewed", "app.user.get.app_error", nil, "", http.StatusInternalServerError).Wrap(err) + } - notify := member.NotifyProps[model.PushNotifyProp] - if notify == model.ChannelNotifyDefault { - user, err := a.GetUser(userID) - if err != nil { - c.Logger().Warn("Failed to get user", mlog.String("user_id", userID), mlog.Err(err)) - continue - } - notify = user.NotifyProps[model.PushNotifyProp] - } - if notify == model.UserNotifyAll { - if count, err := a.Srv().Store().User().GetAnyUnreadPostCountForChannel(userID, channelID); err == nil { - if count > 0 { - channelsToClearPushNotifications = append(channelsToClearPushNotifications, channelID) - } - } - } else if notify == model.UserNotifyMention || channel.Type == model.ChannelTypeDirect { - if count, err := a.Srv().Store().User().GetUnreadCountForChannel(userID, channelID); err == nil { - if count > 0 { - channelsToClearPushNotifications = append(channelsToClearPushNotifications, channelID) - } - } - } - } + // We use channelsToView to later only update those, or early return if no channel is to be read + channelsToView, channelsToClearPushNotifications, times, err := a.Srv().Store().Channel().GetChannelsWithUnreadsAndWithMentions(c.Context(), channelIDs, userID, user.NotifyProps) + if err != nil { + return nil, model.NewAppError("MarkChannelsAsViewed", "app.channel.get_channels_with_unreads_and_with_mentions.app_error", nil, "", http.StatusInternalServerError).Wrap(err) + } + + if len(channelsToView) == 0 { + return times, nil } - var err error updateThreads := *a.Config().ServiceSettings.ThreadAutoFollow && (!collapsedThreadsSupported || !isCRTEnabled) if updateThreads { - err = a.Srv().Store().Thread().MarkAllAsReadByChannels(userID, channelIDs) + err = a.Srv().Store().Thread().MarkAllAsReadByChannels(userID, channelsToView) if err != nil { - return nil, model.NewAppError("MarkChannelsAsViewed", "app.channel.update_last_viewed_at.app_error", nil, "", http.StatusInternalServerError).Wrap(err) + return nil, model.NewAppError("MarkChannelsAsViewed", "app.thread.mark_all_as_read_by_channels.app_error", nil, "", http.StatusInternalServerError).Wrap(err) } } - times, err := a.Srv().Store().Channel().UpdateLastViewedAt(channelIDs, userID) + _, err = a.Srv().Store().Channel().UpdateLastViewedAt(channelsToView, userID) if err != nil { var invErr *store.ErrInvalidInput switch { @@ -3032,19 +3009,18 @@ func (a *App) MarkChannelsAsViewed(c request.CTX, channelIDs []string, userID st } if *a.Config().ServiceSettings.EnableChannelViewedMessages { - for _, channelID := range channelIDs { - message := model.NewWebSocketEvent(model.WebsocketEventChannelViewed, "", "", userID, nil, "") - message.Add("channel_id", channelID) - a.Publish(message) - } + message := model.NewWebSocketEvent(model.WebsocketEventMultipleChannelsViewed, "", "", userID, nil, "") + message.Add("channel_times", times) + a.Publish(message) } + for _, channelID := range channelsToClearPushNotifications { a.clearPushNotification(currentSessionId, userID, channelID, "") } if updateThreads && isCRTEnabled { timestamp := model.GetMillis() - for _, channelID := range channelIDs { + for _, channelID := range channelsToView { message := model.NewWebSocketEvent(model.WebsocketEventThreadReadChanged, "", channelID, userID, nil, "") message.Add("timestamp", timestamp) a.Publish(message) @@ -3504,6 +3480,231 @@ func (a *App) getDirectChannel(c request.CTX, userID, otherUserID string) (*mode return a.Srv().getDirectChannel(c, userID, otherUserID) } +func (a *App) GetGroupMessageMembersCommonTeams(c request.CTX, channelID string) ([]*model.Team, *model.AppError) { + channel, appErr := a.GetChannel(c, channelID) + if appErr != nil { + return nil, appErr + } + + if channel.Type != model.ChannelTypeGroup { + return nil, model.NewAppError("GetGroupMessageMembersCommonTeams", "app.channel.get_common_teams.incorrect_channel_type", nil, "", http.StatusBadRequest) + } + + users, appErr := a.GetUsersInChannel(&model.UserGetOptions{ + PerPage: model.ChannelGroupMaxUsers, + Page: 0, + InChannelId: channelID, + Inactive: false, + Active: true, + }) + + var userIDs = make([]string, len(users)) + for i := 0; i < len(users); i++ { + userIDs[i] = users[i].Id + } + + commonTeamIDs, err := a.Srv().Store().Team().GetCommonTeamIDsForMultipleUsers(userIDs) + if err != nil { + return nil, model.NewAppError("GetGroupMessageMembersCommonTeams", "app.channel.get_common_teams.store_get_common_teams_error", nil, "", http.StatusInternalServerError).Wrap(err) + } + + teams := []*model.Team{} + if len(commonTeamIDs) > 0 { + teams, appErr = a.GetTeams(commonTeamIDs) + } + + return teams, appErr +} + +func (a *App) ConvertGroupMessageToChannel(c request.CTX, convertedByUserId string, gmConversionRequest *model.GroupMessageConversionRequestBody) (*model.Channel, *model.AppError) { + originalChannel, appErr := a.GetChannel(c, gmConversionRequest.ChannelID) + if appErr != nil { + return nil, appErr + } + + appErr = a.validateForConvertGroupMessageToChannel(c, convertedByUserId, originalChannel, gmConversionRequest) + if appErr != nil { + return nil, appErr + } + + toUpdate := originalChannel.DeepCopy() + toUpdate.Type = model.ChannelTypePrivate + toUpdate.TeamId = gmConversionRequest.TeamID + toUpdate.Name = gmConversionRequest.Name + toUpdate.DisplayName = gmConversionRequest.DisplayName + + updatedChannel, appErr := a.UpdateChannel(c, toUpdate) + if appErr != nil { + return nil, appErr + } + + a.Srv().Platform().InvalidateCacheForChannel(originalChannel) + + users, appErr := a.GetUsersInChannelPage(&model.UserGetOptions{ + InChannelId: gmConversionRequest.ChannelID, + Page: 0, + PerPage: model.ChannelGroupMaxUsers, + }, false) + if appErr != nil { + return nil, appErr + } + + _ = a.setSidebarCategoriesForConvertedGroupMessage(c, gmConversionRequest, users) + _ = a.postMessageForConvertGroupMessageToChannel(c, gmConversionRequest.ChannelID, convertedByUserId, users) + return updatedChannel, nil +} + +func (a *App) setSidebarCategoriesForConvertedGroupMessage(c request.CTX, gmConversionRequest *model.GroupMessageConversionRequestBody, channelUsers []*model.User) *model.AppError { + // First we'll delete channel from everyone's sidebar. Only the members of GM + // can have it in sidebar, so we can delete the channel from all SidebarChannels entries. + err := a.Srv().Store().Channel().DeleteAllSidebarChannelForChannel(gmConversionRequest.ChannelID) + if err != nil { + return model.NewAppError( + "setSidebarCategoriesForConvertedGroupMessage", + "app.channel.gm_conversion_set_categories.delete_all.error", + nil, + "", + http.StatusInternalServerError, + ).Wrap(err) + } + + // Now that we've deleted existing entries, we can set the channel in default "Channels" category + // for all GM members + for _, user := range channelUsers { + categories, appErr := a.GetSidebarCategories(c, user.Id, &store.SidebarCategorySearchOpts{ + TeamID: gmConversionRequest.TeamID, + Type: model.SidebarCategoryChannels, + }) + + if appErr != nil { + mlog.Error("Failed to search sidebar categories for user for adding converted GM") + continue + } + + if len(categories.Categories) < 1 { + // It is normal for user to not have the default category. + // The default "Channels" category is created when the user first logs in, + // and all their channels are moved to this category at the same time. + // So its perfectly okay for this condition to occur. + continue + } + + // when we fetch the default "Channels" category from store layer, + // it auto-fills any channels the user has access to but aren't associated to a category in the database. + // So what we do is fetch the category, so we get an auto-filled data, + // then call update category to persist the data and send the websocket events. + channelsCategory := categories.Categories[0] + _, appErr = a.UpdateSidebarCategories(c, user.Id, gmConversionRequest.TeamID, []*model.SidebarCategoryWithChannels{channelsCategory}) + if appErr != nil { + mlog.Error("Failed to add converted GM to default sidebar category for user", mlog.String("user_id", user.Id), mlog.Err(err)) + } + } + + return nil +} + +func (a *App) validateForConvertGroupMessageToChannel(c request.CTX, convertedByUserId string, originalChannel *model.Channel, gmConversionRequest *model.GroupMessageConversionRequestBody) *model.AppError { + commonTeams, appErr := a.GetGroupMessageMembersCommonTeams(c, originalChannel.Id) + if appErr != nil { + return appErr + } + + teamFound := false + for _, team := range commonTeams { + if team.Id == gmConversionRequest.TeamID { + teamFound = true + break + } + } + + if !teamFound { + return model.NewAppError( + "validateForConvertGroupMessageToChannel", + "app.channel.group_message_conversion.incorrect_team", + nil, + "", + http.StatusBadRequest, + ) + } + + if originalChannel.Type != model.ChannelTypeGroup { + return model.NewAppError( + "ConvertGroupMessageToChannel", + "app.channel.group_message_conversion.original_channel_not_gm", + nil, + "", + http.StatusNotFound, + ) + } + + channelMember, appErr := a.GetChannelMember(c, gmConversionRequest.ChannelID, convertedByUserId) + if appErr != nil { + return appErr + } + + if channelMember == nil { + return model.NewAppError("ConvertGroupMessageToChannel", "app.channel.group_message_conversion.channel_member_missing", nil, "", http.StatusNotFound) + } + + // apply dummy changes to check validity + clone := originalChannel.DeepCopy() + clone.Type = model.ChannelTypePrivate + clone.Name = gmConversionRequest.Name + clone.DisplayName = gmConversionRequest.DisplayName + return clone.IsValid() +} + +func (a *App) postMessageForConvertGroupMessageToChannel(c request.CTX, channelID, convertedByUserId string, channelUsers []*model.User) *model.AppError { + convertedByUser, appErr := a.GetUser(convertedByUserId) + if appErr != nil { + return appErr + } + + userIDs := make([]string, len(channelUsers)) + usernames := make([]string, len(channelUsers)) + for i, user := range channelUsers { + userIDs[i] = user.Id + usernames[i] = user.Username + } + + message := i18n.T( + "api.channel.group_message.converted.to_private_channel", + map[string]any{ + "ConvertedByUsername": convertedByUser.Username, + "GMMembers": utils.JoinList(usernames), + }) + + post := &model.Post{ + ChannelId: channelID, + Message: message, + Type: model.PostTypeGMConvertedToChannel, + UserId: convertedByUserId, + } + + // these props are used for re-constructing a localized message on the client + post.AddProp("convertedByUserId", convertedByUser.Id) + post.AddProp("gmMembersDuringConversionIDs", userIDs) + + channel, appErr := a.GetChannel(c, channelID) + if appErr != nil { + return appErr + } + + if _, appErr := a.CreatePost(c, post, channel, false, true); appErr != nil { + mlog.Error("Failed to create post for notifying about GM converted to private channel", mlog.Err(appErr)) + + return model.NewAppError( + "postMessageForConvertGroupMessageToChannel", + "app.channel.group_message_conversion.post_message.error", + nil, + "", + http.StatusInternalServerError, + ).Wrap(appErr) + } + + return nil +} + func (s *Server) getDirectChannel(c request.CTX, userID, otherUserID string) (*model.Channel, *model.AppError) { channel, nErr := s.Store().Channel().GetByName("", model.GetDMNameFromIds(userID, otherUserID), true) if nErr != nil { diff --git a/server/channels/app/channel_category.go b/server/channels/app/channel_category.go index 7043abe627b..05ddf6888b7 100644 --- a/server/channels/app/channel_category.go +++ b/server/channels/app/channel_category.go @@ -10,7 +10,7 @@ import ( "github.com/mattermost/mattermost/server/public/model" "github.com/mattermost/mattermost/server/public/shared/mlog" - "github.com/mattermost/mattermost/server/v8/channels/app/request" + "github.com/mattermost/mattermost/server/public/shared/request" "github.com/mattermost/mattermost/server/v8/channels/store" ) diff --git a/server/channels/app/channel_test.go b/server/channels/app/channel_test.go index a6c3c57ad44..cc45e447249 100644 --- a/server/channels/app/channel_test.go +++ b/server/channels/app/channel_test.go @@ -7,18 +7,23 @@ import ( "context" "errors" "fmt" - "net/http" "sort" "strings" "sync" "testing" + "time" + + "github.com/mattermost/mattermost/server/v8/channels/store" + + "github.com/mattermost/mattermost/server/v8/channels/app/teams" + "github.com/mattermost/mattermost/server/v8/channels/app/users" + "github.com/mattermost/mattermost/server/v8/channels/store/sqlstore" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" "github.com/mattermost/mattermost/server/public/model" - "github.com/mattermost/mattermost/server/v8/channels/app/users" "github.com/mattermost/mattermost/server/v8/channels/store/storetest/mocks" ) @@ -2107,48 +2112,6 @@ func TestPatchChannelModerationsForChannel(t *testing.T) { }) } -// TestMarkChannelsAsViewedPanic verifies that returning an error from a.GetUser -// does not cause a panic. -func TestMarkChannelsAsViewedPanic(t *testing.T) { - th := SetupWithStoreMock(t) - defer th.TearDown() - - mockStore := th.App.Srv().Store().(*mocks.Store) - mockUserStore := mocks.UserStore{} - mockUserStore.On("Get", context.Background(), "userID").Return(nil, model.NewAppError("SqlUserStore.Get", "app.user.get.app_error", nil, "user_id=userID", http.StatusInternalServerError)) - mockChannelStore := mocks.ChannelStore{} - mockChannelStore.On("Get", "channelID", true).Return(&model.Channel{}, nil) - mockChannelStore.On("GetMember", context.Background(), "channelID", "userID").Return(&model.ChannelMember{ - NotifyProps: model.StringMap{ - model.PushNotifyProp: model.ChannelNotifyDefault, - }}, nil) - times := map[string]int64{ - "userID": 1, - } - mockChannelStore.On("UpdateLastViewedAt", []string{"channelID"}, "userID").Return(times, nil) - mockSessionStore := mocks.SessionStore{} - mockOAuthStore := mocks.OAuthStore{} - var err error - th.App.ch.srv.userService, err = users.New(users.ServiceConfig{ - UserStore: &mockUserStore, - SessionStore: &mockSessionStore, - OAuthStore: &mockOAuthStore, - ConfigFn: th.App.ch.srv.platform.Config, - LicenseFn: th.App.ch.srv.License, - }) - require.NoError(t, err) - mockPreferenceStore := mocks.PreferenceStore{} - mockPreferenceStore.On("Get", mock.AnythingOfType("string"), mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return(&model.Preference{Value: "test"}, nil) - mockStore.On("Channel").Return(&mockChannelStore) - mockStore.On("Preference").Return(&mockPreferenceStore) - mockThreadStore := mocks.ThreadStore{} - mockThreadStore.On("MarkAllAsReadByChannels", "userID", []string{"channelID"}).Return(nil) - mockStore.On("Thread").Return(&mockThreadStore) - - _, appErr := th.App.MarkChannelsAsViewed(th.Context, []string{"channelID"}, "userID", th.Context.Session().Id, false, false) - require.Nil(t, appErr) -} - func TestClearChannelMembersCache(t *testing.T) { th := SetupWithStoreMock(t) defer th.TearDown() @@ -2494,3 +2457,210 @@ func TestIsCRTEnabledForUser(t *testing.T) { }) } } + +func TestGetGroupMessageMembersCommonTeams(t *testing.T) { + th := SetupWithStoreMock(t) + defer th.TearDown() + + mockStore := th.App.Srv().Store().(*mocks.Store) + + mockChannelStore := mocks.ChannelStore{} + mockStore.On("Channel").Return(&mockChannelStore) + mockChannelStore.On("Get", "gm_channel_id", true).Return(&model.Channel{Type: model.ChannelTypeGroup}, nil) + + mockTeamStore := mocks.TeamStore{} + mockStore.On("Team").Return(&mockTeamStore) + + th.App.Srv().Store().Team() + + mockTeamStore.On("GetCommonTeamIDsForMultipleUsers", []string{"user_id_1", "user_id_2"}).Return([]string{"team_id_1", "team_id_2", "team_id_3"}, nil).Times(1) + mockTeamStore.On("GetMany", []string{"team_id_1", "team_id_2", "team_id_3"}).Return( + []*model.Team{ + {DisplayName: "Team 1"}, + {DisplayName: "Team 2"}, + {DisplayName: "Team 3"}, + }, + nil, + ) + + mockUserStore := mocks.UserStore{} + mockStore.On("User").Return(&mockUserStore) + options := &model.UserGetOptions{ + PerPage: model.ChannelGroupMaxUsers, + Page: 0, + InChannelId: "gm_channel_id", + Inactive: false, + Active: true, + } + mockUserStore.On("GetProfilesInChannel", options).Return([]*model.User{ + { + Id: "user_id_1", + }, + { + Id: "user_id_2", + }, + }, nil) + + var err error + th.App.ch.srv.teamService, err = teams.New(teams.ServiceConfig{ + TeamStore: &mockTeamStore, + ChannelStore: &mockChannelStore, + GroupStore: &mocks.GroupStore{}, + Users: th.App.ch.srv.userService, + WebHub: th.App.ch.srv.platform, + ConfigFn: th.App.ch.srv.platform.Config, + LicenseFn: th.App.ch.srv.License, + }) + require.NoError(t, err) + + commonTeams, appErr := th.App.GetGroupMessageMembersCommonTeams(th.Context, "gm_channel_id") + require.Nil(t, appErr) + require.Equal(t, 3, len(commonTeams)) + + // case of no common teams + mockTeamStore.On("GetCommonTeamIDsForMultipleUsers", []string{"user_id_1", "user_id_2"}).Return([]string{}, nil) + commonTeams, appErr = th.App.GetGroupMessageMembersCommonTeams(th.Context, "gm_channel_id") + require.Nil(t, appErr) + require.Equal(t, 0, len(commonTeams)) +} + +func TestConvertGroupMessageToChannel(t *testing.T) { + th := SetupWithStoreMock(t) + defer th.TearDown() + + mockStore := th.App.Srv().Store().(*mocks.Store) + + mockChannelStore := mocks.ChannelStore{} + mockStore.On("Channel").Return(&mockChannelStore) + mockChannelStore.On("Get", "channelidchannelidchanneli", true).Return(&model.Channel{ + Id: "channelidchannelidchanneli", + CreateAt: time.Now().Unix(), + UpdateAt: time.Now().Unix(), + Type: model.ChannelTypeGroup, + }, nil) + mockChannelStore.On("Update", mock.AnythingOfType("*model.Channel")).Return(&model.Channel{}, nil) + mockChannelStore.On("InvalidateChannel", "channelidchannelidchanneli") + mockChannelStore.On("InvalidateChannelByName", "team_id_1", "new_name").Times(1) + mockChannelStore.On("InvalidateChannelByName", "dm", "") + mockChannelStore.On("GetMember", sqlstore.WithMaster(context.Background()), "channelidchannelidchanneli", "user_id_1").Return(&model.ChannelMember{}, nil).Times(1) + mockChannelStore.On("GetMember", context.Background(), "channelidchannelidchanneli", "user_id_1").Return(&model.ChannelMember{}, nil).Times(1) + mockChannelStore.On("InvalidatePinnedPostCount", "channelidchannelidchanneli") + mockChannelStore.On("GetAllChannelMembersNotifyPropsForChannel", "channelidchannelidchanneli", true).Return(map[string]model.StringMap{}, nil) + mockChannelStore.On("IncrementMentionCount", "", []string{}, true, false).Return(nil) + mockChannelStore.On("DeleteAllSidebarChannelForChannel", "channelidchannelidchanneli").Return(nil) + mockChannelStore.On("GetSidebarCategories", "user_id_1", &store.SidebarCategorySearchOpts{TeamID: "team_id_1", ExcludeTeam: false, Type: "channels"}).Return( + &model.OrderedSidebarCategories{ + Categories: model.SidebarCategoriesWithChannels{ + { + SidebarCategory: model.SidebarCategory{ + Type: model.SidebarCategoryChannels, + }, + }, + }, + }, nil) + mockChannelStore.On("GetSidebarCategories", "user_id_2", &store.SidebarCategorySearchOpts{TeamID: "team_id_1", ExcludeTeam: false, Type: "channels"}).Return( + &model.OrderedSidebarCategories{ + Categories: model.SidebarCategoriesWithChannels{ + { + SidebarCategory: model.SidebarCategory{ + Type: model.SidebarCategoryChannels, + }, + }, + }, + }, nil) + mockChannelStore.On("UpdateSidebarCategories", "user_id_1", "team_id_1", mock.Anything).Return( + []*model.SidebarCategoryWithChannels{ + { + SidebarCategory: model.SidebarCategory{ + Type: model.SidebarCategoryChannels, + }, + }, + }, + []*model.SidebarCategoryWithChannels{ + { + SidebarCategory: model.SidebarCategory{ + Type: model.SidebarCategoryChannels, + }, + }, + }, + nil, + ) + mockChannelStore.On("UpdateSidebarCategories", "user_id_2", "team_id_1", mock.Anything).Return( + []*model.SidebarCategoryWithChannels{ + { + SidebarCategory: model.SidebarCategory{ + Type: model.SidebarCategoryChannels, + }, + }, + }, + []*model.SidebarCategoryWithChannels{ + { + SidebarCategory: model.SidebarCategory{ + Type: model.SidebarCategoryChannels, + }, + }, + }, + nil, + ) + + mockTeamStore := mocks.TeamStore{} + mockStore.On("Team").Return(&mockTeamStore) + mockTeamStore.On("GetMember", sqlstore.WithMaster(context.Background()), "team_id_1", "user_id_1").Return(&model.TeamMember{}, nil) + mockTeamStore.On("GetCommonTeamIDsForMultipleUsers", []string{"user_id_1", "user_id_2"}).Return([]string{"team_id_1", "team_id_2", "team_id_3"}, nil).Times(1) + mockTeamStore.On("GetMany", []string{"team_id_1", "team_id_2", "team_id_3"}).Return( + []*model.Team{ + {Id: "team_id_1", DisplayName: "Team 1"}, + {Id: "team_id_2", DisplayName: "Team 2"}, + {Id: "team_id_3", DisplayName: "Team 3"}, + }, + nil, + ) + + mockUserStore := mocks.UserStore{} + mockStore.On("User").Return(&mockUserStore) + mockUserStore.On("Get", context.Background(), "user_id_1").Return(&model.User{Username: "username_1"}, nil) + mockUserStore.On("GetProfilesInChannel", mock.AnythingOfType("*model.UserGetOptions")).Return([]*model.User{ + {Id: "user_id_1", Username: "user_id_1"}, + {Id: "user_id_2", Username: "user_id_2"}, + }, nil) + mockUserStore.On("GetAllProfilesInChannel", mock.Anything, mock.Anything, mock.Anything).Return(map[string]*model.User{}, nil) + + mockPostStore := mocks.PostStore{} + mockStore.On("Post").Return(&mockPostStore) + mockPostStore.On("Save", mock.AnythingOfType("*model.Post")).Return(&model.Post{}, nil) + mockPostStore.On("InvalidateLastPostTimeCache", "channelidchannelidchanneli") + + var err error + + th.App.ch.srv.userService, err = users.New(users.ServiceConfig{ + UserStore: &mockUserStore, + ConfigFn: th.App.ch.srv.platform.Config, + SessionStore: &mocks.SessionStore{}, + OAuthStore: &mocks.OAuthStore{}, + LicenseFn: th.App.ch.srv.License, + }) + require.NoError(t, err) + + th.App.ch.srv.teamService, err = teams.New(teams.ServiceConfig{ + TeamStore: &mockTeamStore, + ChannelStore: &mockChannelStore, + GroupStore: &mocks.GroupStore{}, + Users: th.App.ch.srv.userService, + WebHub: th.App.ch.srv.platform, + ConfigFn: th.App.ch.srv.platform.Config, + LicenseFn: th.App.ch.srv.License, + }) + require.NoError(t, err) + + conversionRequest := &model.GroupMessageConversionRequestBody{ + ChannelID: "channelidchannelidchanneli", + TeamID: "team_id_1", + Name: "new_name", + DisplayName: "New Display Name", + } + + convertedChannel, appErr := th.App.ConvertGroupMessageToChannel(th.Context, "user_id_1", conversionRequest) + require.Nil(t, appErr) + require.Equal(t, model.ChannelTypePrivate, convertedChannel.Type) + +} diff --git a/server/channels/app/channels.go b/server/channels/app/channels.go index 7e5f94508f2..b134adf8882 100644 --- a/server/channels/app/channels.go +++ b/server/channels/app/channels.go @@ -14,8 +14,8 @@ import ( "github.com/mattermost/mattermost/server/public/model" "github.com/mattermost/mattermost/server/public/plugin" "github.com/mattermost/mattermost/server/public/shared/mlog" + "github.com/mattermost/mattermost/server/public/shared/request" "github.com/mattermost/mattermost/server/v8/channels/app/imaging" - "github.com/mattermost/mattermost/server/v8/channels/app/request" "github.com/mattermost/mattermost/server/v8/channels/product" "github.com/mattermost/mattermost/server/v8/config" "github.com/mattermost/mattermost/server/v8/einterfaces" @@ -44,11 +44,12 @@ type Channels struct { postActionCookieSecret []byte - pluginCommandsLock sync.RWMutex - pluginCommands []*PluginCommand - pluginsLock sync.RWMutex - pluginsEnvironment *plugin.Environment - pluginConfigListenerID string + pluginCommandsLock sync.RWMutex + pluginCommands []*PluginCommand + pluginsLock sync.RWMutex + pluginsEnvironment *plugin.Environment + pluginConfigListenerID string + pluginClusterLeaderListenerID string productCommandsLock sync.RWMutex productCommands []*ProductCommand diff --git a/server/channels/app/command.go b/server/channels/app/command.go index 8f4400565a5..4cff1135e67 100644 --- a/server/channels/app/command.go +++ b/server/channels/app/command.go @@ -17,7 +17,7 @@ import ( "github.com/mattermost/mattermost/server/public/model" "github.com/mattermost/mattermost/server/public/shared/i18n" "github.com/mattermost/mattermost/server/public/shared/mlog" - "github.com/mattermost/mattermost/server/v8/channels/app/request" + "github.com/mattermost/mattermost/server/public/shared/request" "github.com/mattermost/mattermost/server/v8/channels/store" ) diff --git a/server/channels/app/command_autocomplete.go b/server/channels/app/command_autocomplete.go index e90d5c30417..f76d3e27674 100644 --- a/server/channels/app/command_autocomplete.go +++ b/server/channels/app/command_autocomplete.go @@ -13,7 +13,7 @@ import ( "github.com/mattermost/mattermost/server/public/model" "github.com/mattermost/mattermost/server/public/shared/mlog" - "github.com/mattermost/mattermost/server/v8/channels/app/request" + "github.com/mattermost/mattermost/server/public/shared/request" ) // AutocompleteDynamicArgProvider dynamically provides auto-completion args for built-in commands. diff --git a/server/channels/app/command_autocomplete_test.go b/server/channels/app/command_autocomplete_test.go index 441576165c5..7c133ad1593 100644 --- a/server/channels/app/command_autocomplete_test.go +++ b/server/channels/app/command_autocomplete_test.go @@ -11,7 +11,7 @@ import ( "github.com/mattermost/mattermost/server/public/model" "github.com/mattermost/mattermost/server/public/shared/i18n" - "github.com/mattermost/mattermost/server/v8/channels/app/request" + "github.com/mattermost/mattermost/server/public/shared/request" ) func TestParseStaticListArgument(t *testing.T) { diff --git a/server/channels/app/context.go b/server/channels/app/context.go index 26775422efb..cd5cab3aa13 100644 --- a/server/channels/app/context.go +++ b/server/channels/app/context.go @@ -7,7 +7,7 @@ import ( "context" "github.com/mattermost/mattermost/server/public/plugin" - "github.com/mattermost/mattermost/server/v8/channels/app/request" + "github.com/mattermost/mattermost/server/public/shared/request" "github.com/mattermost/mattermost/server/v8/channels/store/sqlstore" ) diff --git a/server/channels/app/desktop_login.go b/server/channels/app/desktop_login.go new file mode 100644 index 00000000000..6d089dbcab2 --- /dev/null +++ b/server/channels/app/desktop_login.go @@ -0,0 +1,48 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package app + +import ( + "net/http" + + "github.com/mattermost/mattermost/server/public/model" +) + +func (a *App) GenerateAndSaveDesktopToken(createAt int64, user *model.User) (*string, *model.AppError) { + token := model.NewRandomString(64) + err := a.Srv().Store().DesktopTokens().Insert(token, createAt, user.Id) + if err != nil { + // Delete any other related tokens if there's an error + a.Srv().Store().DesktopTokens().DeleteByUserId(user.Id) + + return nil, model.NewAppError("GenerateAndSaveDesktopToken", "app.desktop_token.generateServerToken.invalid_or_expired", nil, err.Error(), http.StatusBadRequest) + } + + return &token, nil +} + +func (a *App) ValidateDesktopToken(token string, expiryTime int64) (*model.User, *model.AppError) { + // Check if token is valid + userId, err := a.Srv().Store().DesktopTokens().GetUserId(token, expiryTime) + if err != nil { + // Delete the token if it is expired or invalid + a.Srv().Store().DesktopTokens().Delete(token) + + return nil, model.NewAppError("ValidateDesktopToken", "app.desktop_token.validate.invalid", nil, err.Error(), http.StatusUnauthorized) + } + + // Get the user profile + user, userErr := a.GetUser(*userId) + if userErr != nil { + // Delete the token if the user is invalid somehow + a.Srv().Store().DesktopTokens().Delete(token) + + return nil, model.NewAppError("ValidateDesktopToken", "app.desktop_token.validate.no_user", nil, userErr.Error(), http.StatusInternalServerError) + } + + // Clean up other tokens if they exist + a.Srv().Store().DesktopTokens().DeleteByUserId(*userId) + + return user, nil +} diff --git a/server/channels/app/desktop_login_test.go b/server/channels/app/desktop_login_test.go new file mode 100644 index 00000000000..b4726fe1032 --- /dev/null +++ b/server/channels/app/desktop_login_test.go @@ -0,0 +1,75 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package app + +import ( + "testing" + "time" + + "github.com/mattermost/mattermost/server/public/model" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +const ( + TTL = time.Minute * 3 + ExpiredLength = time.Minute * 10 +) + +func TestGenerateAndSaveDesktopToken(t *testing.T) { + th := Setup(t).InitBasic() + defer th.TearDown() + + t.Run("generate token", func(t *testing.T) { + token, err := th.App.GenerateAndSaveDesktopToken(time.Now().Unix(), th.BasicUser) + assert.Nil(t, err) + assert.NotNil(t, token) + }) +} + +func TestValidateDesktopToken(t *testing.T) { + th := Setup(t).InitBasic() + defer th.TearDown() + + authenticatedServerToken, err := th.App.GenerateAndSaveDesktopToken(time.Now().Unix(), th.BasicUser) + require.Nil(t, err) + require.NotNil(t, authenticatedServerToken) + + expiredServerToken, err := th.App.GenerateAndSaveDesktopToken(time.Now().Add(-ExpiredLength).Unix(), th.BasicUser2) + require.Nil(t, err) + require.NotNil(t, expiredServerToken) + + badUser := model.User{Id: "some_garbage_user_id"} + badUserServerToken, err := th.App.GenerateAndSaveDesktopToken(time.Now().Unix(), &badUser) + require.Nil(t, err) + require.NotNil(t, badUserServerToken) + + t.Run("validate token", func(t *testing.T) { + user, err := th.App.ValidateDesktopToken(*authenticatedServerToken, time.Now().Add(-TTL).Unix()) + assert.Nil(t, err) + assert.NotNil(t, user) + assert.Equal(t, th.BasicUser.Id, user.Id) + }) + + t.Run("validate token - expired", func(t *testing.T) { + user, err := th.App.ValidateDesktopToken(*expiredServerToken, time.Now().Add(-TTL).Unix()) + assert.NotNil(t, err) + assert.Nil(t, user) + assert.Equal(t, "app.desktop_token.validate.invalid", err.Id) + }) + + t.Run("validate token - not authenticated", func(t *testing.T) { + user, err := th.App.ValidateDesktopToken("not_real_token", time.Now().Add(-TTL).Unix()) + assert.NotNil(t, err) + assert.Nil(t, user) + assert.Equal(t, "app.desktop_token.validate.invalid", err.Id) + }) + + t.Run("validate token - bad user id", func(t *testing.T) { + user, err := th.App.ValidateDesktopToken(*badUserServerToken, time.Now().Add(-TTL).Unix()) + assert.NotNil(t, err) + assert.Nil(t, user) + assert.Equal(t, "app.desktop_token.validate.no_user", err.Id) + }) +} diff --git a/server/channels/app/draft.go b/server/channels/app/draft.go index 14f5f19df8a..b50c9109048 100644 --- a/server/channels/app/draft.go +++ b/server/channels/app/draft.go @@ -11,7 +11,7 @@ import ( "github.com/mattermost/mattermost/server/public/model" "github.com/mattermost/mattermost/server/public/shared/mlog" - "github.com/mattermost/mattermost/server/v8/channels/app/request" + "github.com/mattermost/mattermost/server/public/shared/request" "github.com/mattermost/mattermost/server/v8/channels/store" ) @@ -107,11 +107,24 @@ func (a *App) getFileInfosForDraft(draft *model.Draft) ([]*model.FileInfo, *mode return nil, nil } - fileInfos, err := a.Srv().Store().FileInfo().GetByIds(draft.FileIds) + allFileInfos, err := a.Srv().Store().FileInfo().GetByIds(draft.FileIds) if err != nil { return nil, model.NewAppError("GetFileInfosForDraft", "app.draft.get_for_draft.app_error", nil, "", http.StatusInternalServerError).Wrap(err) } + fileInfos := []*model.FileInfo{} + for _, fileInfo := range allFileInfos { + if fileInfo.PostId == "" && fileInfo.CreatorId == draft.UserId { + fileInfos = append(fileInfos, fileInfo) + } else { + mlog.Debug("Invalid file id in draft", mlog.String("file_id", fileInfo.Id), mlog.String("user_id", draft.UserId)) + } + } + + if len(fileInfos) == 0 { + return nil, nil + } + a.generateMiniPreviewForInfos(fileInfos) return fileInfos, nil diff --git a/server/channels/app/draft_test.go b/server/channels/app/draft_test.go index dc8893e7f3c..1e22896a3b2 100644 --- a/server/channels/app/draft_test.go +++ b/server/channels/app/draft_test.go @@ -6,6 +6,7 @@ package app import ( "os" "testing" + "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -246,7 +247,6 @@ func TestGetDraftsForUser(t *testing.T) { draft1 := &model.Draft{ CreateAt: 00001, - UpdateAt: 00001, UserId: user.Id, ChannelId: channel.Id, Message: "draft1", @@ -254,7 +254,6 @@ func TestGetDraftsForUser(t *testing.T) { draft2 := &model.Draft{ CreateAt: 00005, - UpdateAt: 00005, UserId: user.Id, ChannelId: channel2.Id, Message: "draft2", @@ -263,11 +262,13 @@ func TestGetDraftsForUser(t *testing.T) { _, createDraftErr1 := th.App.UpsertDraft(th.Context, draft1, "") assert.Nil(t, createDraftErr1) + // Wait a bit so the second draft gets a newer UpdateAt + time.Sleep(100 * time.Millisecond) + _, createDraftErr2 := th.App.UpsertDraft(th.Context, draft2, "") assert.Nil(t, createDraftErr2) t.Run("get drafts", func(t *testing.T) { - t.Skip("MM-52088") draftResp, err := th.App.GetDraftsForUser(user.Id, th.BasicTeam.Id) assert.Nil(t, err) @@ -283,7 +284,7 @@ func TestGetDraftsForUser(t *testing.T) { sent, readFileErr := testutils.ReadTestFile("test.png") require.NoError(t, readFileErr) - fileResp, updateDraftErr := th.App.UploadFile(th.Context, sent, channel.Id, "test.png") + fileResp, updateDraftErr := th.App.UploadFileForUserAndTeam(th.Context, sent, channel.Id, "test.png", user.Id, "") assert.Nil(t, updateDraftErr) draftWithFiles := draft1 @@ -303,11 +304,47 @@ func TestGetDraftsForUser(t *testing.T) { assert.Equal(t, draftWithFiles.ChannelId, draftsWithFilesResp[0].ChannelId) assert.ElementsMatch(t, draftWithFiles.FileIds, draftsWithFilesResp[0].FileIds) + assert.Len(t, draftsWithFilesResp[0].Metadata.Files, 1) assert.Equal(t, fileResp.Name, draftsWithFilesResp[0].Metadata.Files[0].Name) assert.Len(t, draftsWithFilesResp, 2) }) + t.Run("get draft with invalid files", func(t *testing.T) { + // upload file + sent, readFileErr := testutils.ReadTestFile("test.png") + require.NoError(t, readFileErr) + + fileResp1, updateDraftErr := th.App.UploadFileForUserAndTeam(th.Context, sent, channel.Id, "test1.png", user.Id, "") + assert.Nil(t, updateDraftErr) + + fileResp2, updateDraftErr := th.App.UploadFileForUserAndTeam(th.Context, sent, channel.Id, "test2.png", th.BasicUser2.Id, "") + assert.Nil(t, updateDraftErr) + + draftWithFiles := draft1 + draftWithFiles.FileIds = []string{fileResp1.Id, fileResp2.Id} + + draftResp, updateDraftErr := th.App.UpsertDraft(th.Context, draft1, "") + assert.Nil(t, updateDraftErr) + + assert.Equal(t, draftWithFiles.Message, draftResp.Message) + assert.Equal(t, draftWithFiles.ChannelId, draftResp.ChannelId) + assert.ElementsMatch(t, draftWithFiles.FileIds, draftResp.FileIds) + + assert.Len(t, draftWithFiles.Metadata.Files, 1) + assert.Equal(t, fileResp1.Name, draftWithFiles.Metadata.Files[0].Name) + + draftsWithFilesResp, err := th.App.GetDraftsForUser(user.Id, th.BasicTeam.Id) + assert.Nil(t, err) + + assert.Equal(t, draftWithFiles.Message, draftsWithFilesResp[0].Message) + assert.Equal(t, draftWithFiles.ChannelId, draftsWithFilesResp[0].ChannelId) + assert.ElementsMatch(t, draftWithFiles.FileIds, draftsWithFilesResp[0].FileIds) + + assert.Len(t, draftsWithFilesResp[0].Metadata.Files, 1) + assert.Equal(t, fileResp1.Name, draftsWithFilesResp[0].Metadata.Files[0].Name) + }) + t.Run("get drafts feature flag", func(t *testing.T) { os.Setenv("MM_FEATUREFLAGS_GLOBALDRAFTS", "false") defer os.Unsetenv("MM_FEATUREFLAGS_GLOBALDRAFTS") diff --git a/server/channels/app/emoji.go b/server/channels/app/emoji.go index 273282181d0..31ca5ae8ad7 100644 --- a/server/channels/app/emoji.go +++ b/server/channels/app/emoji.go @@ -5,7 +5,6 @@ package app import ( "bytes" - "context" "encoding/json" "errors" "fmt" @@ -24,7 +23,7 @@ import ( "github.com/mattermost/mattermost/server/public/model" "github.com/mattermost/mattermost/server/public/shared/mlog" - "github.com/mattermost/mattermost/server/v8/channels/app/request" + "github.com/mattermost/mattermost/server/public/shared/request" "github.com/mattermost/mattermost/server/v8/channels/store" "github.com/mattermost/mattermost/server/v8/channels/utils" ) @@ -60,7 +59,7 @@ func (a *App) CreateEmoji(c request.CTX, sessionUserId string, emoji *model.Emoj return nil, model.NewAppError("createEmoji", "api.emoji.create.other_user.app_error", nil, "", http.StatusForbidden) } - if existingEmoji, err := a.Srv().Store().Emoji().GetByName(context.Background(), emoji.Name, true); err == nil && existingEmoji != nil { + if existingEmoji, err := a.Srv().Store().Emoji().GetByName(c, emoji.Name, true); err == nil && existingEmoji != nil { return nil, model.NewAppError("createEmoji", "api.emoji.create.duplicate.app_error", nil, "", http.StatusBadRequest).Wrap(err) } @@ -191,7 +190,7 @@ func (a *App) GetEmoji(c request.CTX, emojiId string) (*model.Emoji, *model.AppE return nil, model.NewAppError("GetEmoji", "api.emoji.storage.app_error", nil, "", http.StatusForbidden) } - emoji, err := a.Srv().Store().Emoji().Get(c.Context(), emojiId, true) + emoji, err := a.Srv().Store().Emoji().Get(c, emojiId, true) if err != nil { var nfErr *store.ErrNotFound switch { @@ -214,7 +213,7 @@ func (a *App) GetEmojiByName(c request.CTX, emojiName string) (*model.Emoji, *mo return nil, model.NewAppError("GetEmojiByName", "api.emoji.storage.app_error", nil, "", http.StatusForbidden) } - emoji, err := a.Srv().Store().Emoji().GetByName(c.Context(), emojiName, true) + emoji, err := a.Srv().Store().Emoji().GetByName(c, emojiName, true) if err != nil { var nfErr *store.ErrNotFound switch { @@ -247,7 +246,7 @@ func (a *App) GetMultipleEmojiByName(c request.CTX, names []string) ([]*model.Em return []*model.Emoji{}, nil } - emoji, err := a.Srv().Store().Emoji().GetMultipleByName(c.Context(), names) + emoji, err := a.Srv().Store().Emoji().GetMultipleByName(c, names) if err != nil { return nil, model.NewAppError("GetMultipleEmojiByName", "app.emoji.get_by_name.app_error", nil, fmt.Sprintf("names=%v, %v", names, err.Error()), http.StatusInternalServerError) } @@ -256,7 +255,7 @@ func (a *App) GetMultipleEmojiByName(c request.CTX, names []string) ([]*model.Em } func (a *App) GetEmojiImage(c request.CTX, emojiId string) ([]byte, string, *model.AppError) { - _, storeErr := a.Srv().Store().Emoji().Get(c.Context(), emojiId, true) + _, storeErr := a.Srv().Store().Emoji().Get(c, emojiId, true) if storeErr != nil { var nfErr *store.ErrNotFound switch { @@ -302,7 +301,7 @@ func (a *App) GetEmojiStaticURL(c request.CTX, emojiName string) (string, *model return path.Join(subPath, "/static/emoji", id+".png"), nil } - emoji, err := a.Srv().Store().Emoji().GetByName(context.Background(), emojiName, true) + emoji, err := a.Srv().Store().Emoji().GetByName(c, emojiName, true) if err == nil { return path.Join(subPath, "/api/v4/emoji", emoji.Id, "image"), nil } diff --git a/server/channels/app/export.go b/server/channels/app/export.go index 95af0ae6bc5..017ad6dd56f 100644 --- a/server/channels/app/export.go +++ b/server/channels/app/export.go @@ -21,8 +21,8 @@ import ( "github.com/mattermost/mattermost/server/public/model" "github.com/mattermost/mattermost/server/public/shared/mlog" + "github.com/mattermost/mattermost/server/public/shared/request" "github.com/mattermost/mattermost/server/v8/channels/app/imports" - "github.com/mattermost/mattermost/server/v8/channels/app/request" "github.com/mattermost/mattermost/server/v8/channels/store" "github.com/mattermost/mattermost/server/v8/platform/shared/filestore" ) @@ -97,17 +97,17 @@ func (a *App) BulkExport(ctx request.CTX, writer io.Writer, outPath string, job } ctx.Logger().Info("Bulk export: exporting channels") - if err = a.exportAllChannels(ctx, job, writer, teamNames); err != nil { + if err = a.exportAllChannels(ctx, job, writer, teamNames, opts.IncludeArchivedChannels); err != nil { return err } ctx.Logger().Info("Bulk export: exporting users") - if err = a.exportAllUsers(ctx, job, writer); err != nil { + if err = a.exportAllUsers(ctx, job, writer, opts.IncludeArchivedChannels); err != nil { return err } ctx.Logger().Info("Bulk export: exporting posts") - attachments, err := a.exportAllPosts(ctx, job, writer, opts.IncludeAttachments) + attachments, err := a.exportAllPosts(ctx, job, writer, opts.IncludeAttachments, opts.IncludeArchivedChannels) if err != nil { return err } @@ -219,7 +219,7 @@ func (a *App) exportAllTeams(ctx request.CTX, job *model.Job, writer io.Writer) return teamNames, nil } -func (a *App) exportAllChannels(ctx request.CTX, job *model.Job, writer io.Writer, teamNames map[string]bool) *model.AppError { +func (a *App) exportAllChannels(ctx request.CTX, job *model.Job, writer io.Writer, teamNames map[string]bool, withArchived bool) *model.AppError { afterId := strings.Repeat("0", 26) cnt := 0 for { @@ -239,7 +239,7 @@ func (a *App) exportAllChannels(ctx request.CTX, job *model.Job, writer io.Write afterId = channel.Id // Skip deleted. - if channel.DeleteAt != 0 { + if channel.DeleteAt != 0 && !withArchived { continue } // Skip channels on deleted teams. @@ -257,7 +257,7 @@ func (a *App) exportAllChannels(ctx request.CTX, job *model.Job, writer io.Write return nil } -func (a *App) exportAllUsers(ctx request.CTX, job *model.Job, writer io.Writer) *model.AppError { +func (a *App) exportAllUsers(ctx request.CTX, job *model.Job, writer io.Writer, includeArchivedChannels bool) *model.AppError { afterId := strings.Repeat("0", 26) cnt := 0 for { @@ -319,7 +319,7 @@ func (a *App) exportAllUsers(ctx request.CTX, job *model.Job, writer io.Writer) userLine.User.NotifyProps = a.buildUserNotifyProps(user.NotifyProps) // Do the Team Memberships. - members, err := a.buildUserTeamAndChannelMemberships(user.Id) + members, err := a.buildUserTeamAndChannelMemberships(user.Id, includeArchivedChannels) if err != nil { return err } @@ -335,7 +335,7 @@ func (a *App) exportAllUsers(ctx request.CTX, job *model.Job, writer io.Writer) return nil } -func (a *App) buildUserTeamAndChannelMemberships(userID string) (*[]imports.UserTeamImportData, *model.AppError) { +func (a *App) buildUserTeamAndChannelMemberships(userID string, includeArchivedChannels bool) (*[]imports.UserTeamImportData, *model.AppError) { var memberships []imports.UserTeamImportData members, err := a.Srv().Store().Team().GetTeamMembersForExport(userID) @@ -353,7 +353,7 @@ func (a *App) buildUserTeamAndChannelMemberships(userID string) (*[]imports.User memberData := ImportUserTeamDataFromTeamMember(member) // Do the Channel Memberships. - channelMembers, err := a.buildUserChannelMemberships(userID, member.TeamId) + channelMembers, err := a.buildUserChannelMemberships(userID, member.TeamId, includeArchivedChannels) if err != nil { return nil, err } @@ -372,8 +372,8 @@ func (a *App) buildUserTeamAndChannelMemberships(userID string) (*[]imports.User return &memberships, nil } -func (a *App) buildUserChannelMemberships(userID string, teamID string) (*[]imports.UserChannelImportData, *model.AppError) { - members, nErr := a.Srv().Store().Channel().GetChannelMembersForExport(userID, teamID) +func (a *App) buildUserChannelMemberships(userID string, teamID string, includeArchivedChannels bool) (*[]imports.UserChannelImportData, *model.AppError) { + members, nErr := a.Srv().Store().Channel().GetChannelMembersForExport(userID, teamID, includeArchivedChannels) if nErr != nil { return nil, model.NewAppError("buildUserChannelMemberships", "app.channel.get_members.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr) } @@ -411,7 +411,7 @@ func (a *App) buildUserNotifyProps(notifyProps model.StringMap) *imports.UserNot } } -func (a *App) exportAllPosts(ctx request.CTX, job *model.Job, writer io.Writer, withAttachments bool) ([]imports.AttachmentImportData, *model.AppError) { +func (a *App) exportAllPosts(ctx request.CTX, job *model.Job, writer io.Writer, withAttachments bool, includeArchivedChannels bool) ([]imports.AttachmentImportData, *model.AppError) { var attachments []imports.AttachmentImportData afterId := strings.Repeat("0", 26) var postProcessCount uint64 @@ -424,7 +424,7 @@ func (a *App) exportAllPosts(ctx request.CTX, job *model.Job, writer io.Writer, logCheckpoint = time.Now() } - posts, nErr := a.Srv().Store().Post().GetParentsForExportAfter(1000, afterId) + posts, nErr := a.Srv().Store().Post().GetParentsForExportAfter(1000, afterId, includeArchivedChannels) if nErr != nil { return nil, model.NewAppError("exportAllPosts", "app.post.get_posts.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr) } diff --git a/server/channels/app/export_converters.go b/server/channels/app/export_converters.go index df903a428c4..f270158ff8d 100644 --- a/server/channels/app/export_converters.go +++ b/server/channels/app/export_converters.go @@ -35,6 +35,7 @@ func ImportLineFromChannel(channel *model.ChannelForExport) *imports.LineImportD Header: &channel.Header, Purpose: &channel.Purpose, Scheme: channel.SchemeName, + DeletedAt: &channel.DeleteAt, }, } } diff --git a/server/channels/app/export_test.go b/server/channels/app/export_test.go index 801fc423a95..dd9190a6fbd 100644 --- a/server/channels/app/export_test.go +++ b/server/channels/app/export_test.go @@ -102,7 +102,7 @@ func TestExportUserChannels(t *testing.T) { require.NoError(t, err) th.App.UpdateChannelMemberNotifyProps(th.Context, notifyProps, channel.Id, user.Id) - exportData, appErr := th.App.buildUserChannelMemberships(user.Id, team.Id) + exportData, appErr := th.App.buildUserChannelMemberships(user.Id, team.Id, false) require.Nil(t, appErr) assert.Equal(t, len(*exportData), 3) for _, data := range *exportData { @@ -767,3 +767,38 @@ func TestExportDeletedTeams(t *testing.T) { assert.NotContains(t, team.Id, team1.Id) } } + +func TestExportArchivedChannels(t *testing.T) { + th1 := Setup(t).InitBasic() + defer th1.TearDown() + + archivedChannel := th1.CreateChannel(th1.Context, th1.BasicTeam) + th1.CreatePost(archivedChannel) + appErr := th1.App.DeleteChannel(th1.Context, archivedChannel, th1.SystemAdminUser.Id) + require.Nil(t, appErr) + + var b bytes.Buffer + appErr = th1.App.BulkExport(th1.Context, &b, "somePath", nil, model.BulkExportOpts{ + IncludeArchivedChannels: true, + }) + require.Nil(t, appErr) + + th2 := Setup(t) + defer th2.TearDown() + err, i := th2.App.BulkImport(th2.Context, &b, nil, false, 5) + assert.Nil(t, err) + assert.Equal(t, 0, i) + + channels2, err := th2.App.GetAllChannels(th1.Context, 0, 10, model.ChannelSearchOpts{ + IncludeDeleted: true, + }) + assert.Nil(t, err) + found := false + for i := range channels2 { + if channels2[i].Name == archivedChannel.Name { + found = true + break + } + } + require.True(t, found, "archived channel not found after import") +} diff --git a/server/channels/app/file.go b/server/channels/app/file.go index 3a37b8b1e7a..e654100f16b 100644 --- a/server/channels/app/file.go +++ b/server/channels/app/file.go @@ -27,8 +27,8 @@ import ( "github.com/mattermost/mattermost/server/public/model" "github.com/mattermost/mattermost/server/public/plugin" "github.com/mattermost/mattermost/server/public/shared/mlog" + "github.com/mattermost/mattermost/server/public/shared/request" "github.com/mattermost/mattermost/server/v8/channels/app/imaging" - "github.com/mattermost/mattermost/server/v8/channels/app/request" "github.com/mattermost/mattermost/server/v8/channels/product" "github.com/mattermost/mattermost/server/v8/channels/store" "github.com/mattermost/mattermost/server/v8/channels/utils" @@ -555,13 +555,27 @@ func GeneratePublicLinkHash(fileID, salt string) string { // UploadFile uploads a single file in form of a completely constructed byte array for a channel. func (a *App) UploadFile(c request.CTX, data []byte, channelID string, filename string) (*model.FileInfo, *model.AppError) { + return a.UploadFileForUserAndTeam(c, data, channelID, filename, "", "") +} + +func (a *App) UploadFileForUserAndTeam(c request.CTX, data []byte, channelID string, filename string, rawUserId string, rawTeamId string) (*model.FileInfo, *model.AppError) { _, err := a.GetChannel(c, channelID) if err != nil && channelID != "" { return nil, model.NewAppError("UploadFile", "api.file.upload_file.incorrect_channelId.app_error", map[string]any{"channelId": channelID}, "", http.StatusBadRequest) } - info, _, appError := a.DoUploadFileExpectModification(c, time.Now(), "noteam", channelID, "nouser", filename, data) + userId := rawUserId + if userId == "" { + userId = "nouser" + } + + teamId := rawTeamId + if teamId == "" { + teamId = "noteam" + } + + info, _, appError := a.DoUploadFileExpectModification(c, time.Now(), teamId, channelID, userId, filename, data) if appError != nil { return nil, appError } @@ -1210,6 +1224,26 @@ func (a *App) GetFileInfo(fileID string) (*model.FileInfo, *model.AppError) { return fileInfo, appErr } +func (a *App) SetFileSearchableContent(fileID string, data string) *model.AppError { + fileInfo, appErr := a.Srv().getFileInfo(fileID) + if appErr != nil { + return appErr + } + + err := a.Srv().Store().FileInfo().SetContent(fileInfo.Id, data) + if err != nil { + var nfErr *store.ErrNotFound + switch { + case errors.As(err, &nfErr): + return model.NewAppError("SetFileSearchableContent", "app.file_info.set_searchable_content.app_error", nil, "", http.StatusNotFound).Wrap(err) + default: + return model.NewAppError("SetFileSearchableContent", "app.file_info.set_searchable_content.app_error", nil, "", http.StatusInternalServerError).Wrap(err) + } + } + + return nil +} + func (a *App) getFileInfoIgnoreCloudLimit(fileID string) (*model.FileInfo, *model.AppError) { fileInfo, appErr := a.Srv().getFileInfo(fileID) if appErr == nil { diff --git a/server/channels/app/file_test.go b/server/channels/app/file_test.go index 06977c11f10..aa4a7024ddd 100644 --- a/server/channels/app/file_test.go +++ b/server/channels/app/file_test.go @@ -650,3 +650,30 @@ func TestComputeLastAccessibleFileTime(t *testing.T) { }) } + +func TestSetFileSearchableContent(t *testing.T) { + th := Setup(t).InitBasic() + defer th.TearDown() + + fileInfo, err := th.App.Srv().Store().FileInfo().Save(&model.FileInfo{ + CreatorId: th.BasicUser.Id, + PostId: th.BasicPost.Id, + ChannelId: th.BasicPost.ChannelId, + Name: "test", + Path: "test", + Extension: "jpg", + MimeType: "image/jpeg", + }) + require.NoError(t, err) + + result, appErr := th.App.SearchFilesInTeamForUser(th.Context, "searchable", th.BasicUser.Id, th.BasicTeam.Id, false, false, 0, 0, 60) + require.Nil(t, appErr) + assert.Equal(t, 0, len(result.Order)) + + appErr = th.App.SetFileSearchableContent(fileInfo.Id, "searchable") + require.Nil(t, appErr) + + result, appErr = th.App.SearchFilesInTeamForUser(th.Context, "searchable", th.BasicUser.Id, th.BasicTeam.Id, false, false, 0, 0, 60) + require.Nil(t, appErr) + assert.Equal(t, 1, len(result.Order)) +} diff --git a/server/channels/app/group.go b/server/channels/app/group.go index c337252f485..d7774902569 100644 --- a/server/channels/app/group.go +++ b/server/channels/app/group.go @@ -24,6 +24,17 @@ func (a *App) GetGroup(id string, opts *model.GetGroupOpts, viewRestrictions *mo } } + if opts != nil && opts.IncludeMemberIDs { + users, err := a.Srv().Store().Group().GetMemberUsers(id) + if err != nil { + return nil, model.NewAppError("GetGroup", "app.member_count", nil, "", http.StatusInternalServerError).Wrap(err) + } + + for _, user := range users { + group.MemberIDs = append(group.MemberIDs, user.Id) + } + } + if opts != nil && opts.IncludeMemberCount { memberCount, err := a.Srv().Store().Group().GetMemberCountWithRestrictions(id, viewRestrictions) if err != nil { @@ -213,6 +224,22 @@ func (a *App) DeleteGroup(groupID string) (*model.Group, *model.AppError) { } } + count, err := a.Srv().Store().Group().GetMemberCount(groupID) + if err != nil { + return nil, model.NewAppError("DeleteGroup", "app.group.id.app_error", nil, "", http.StatusBadRequest).Wrap(err) + } + + deletedGroup.MemberCount = model.NewInt(int(count)) + + messageWs := model.NewWebSocketEvent(model.WebsocketEventReceivedGroup, "", "", "", nil, "") + + groupJSON, err := json.Marshal(deletedGroup) + if err != nil { + return nil, model.NewAppError("DeleteGroup", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err) + } + messageWs.Add("group", string(groupJSON)) + a.Publish(messageWs) + return deletedGroup, nil } @@ -228,6 +255,22 @@ func (a *App) RestoreGroup(groupID string) (*model.Group, *model.AppError) { } } + count, err := a.Srv().Store().Group().GetMemberCount(groupID) + if err != nil { + return nil, model.NewAppError("RestoreGroup", "app.group.id.app_error", nil, "", http.StatusBadRequest).Wrap(err) + } + + restoredGroup.MemberCount = model.NewInt(int(count)) + + messageWs := model.NewWebSocketEvent(model.WebsocketEventReceivedGroup, "", "", "", nil, "") + + groupJSON, err := json.Marshal(restoredGroup) + if err != nil { + return nil, model.NewAppError("RestoreGroup", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err) + } + messageWs.Add("group", string(groupJSON)) + a.Publish(messageWs) + return restoredGroup, nil } @@ -604,6 +647,19 @@ func (a *App) GetGroups(page, perPage int, opts model.GroupSearchOpts, viewRestr return nil, model.NewAppError("GetGroups", "app.select_error", nil, "", http.StatusInternalServerError).Wrap(err) } + if opts.IncludeMemberIDs { + for _, group := range groups { + users, err := a.Srv().Store().Group().GetMemberUsers(group.Id) + if err != nil { + return nil, model.NewAppError("GetGroup", "app.member_count", nil, "", http.StatusInternalServerError).Wrap(err) + } + + for _, user := range users { + group.MemberIDs = append(group.MemberIDs, user.Id) + } + } + } + return groups, nil } diff --git a/server/channels/app/helper_test.go b/server/channels/app/helper_test.go index ff3ce167ce6..e4292b4246d 100644 --- a/server/channels/app/helper_test.go +++ b/server/channels/app/helper_test.go @@ -18,7 +18,7 @@ import ( "github.com/mattermost/mattermost/server/public/model" "github.com/mattermost/mattermost/server/public/plugin" "github.com/mattermost/mattermost/server/public/shared/mlog" - "github.com/mattermost/mattermost/server/v8/channels/app/request" + "github.com/mattermost/mattermost/server/public/shared/request" "github.com/mattermost/mattermost/server/v8/channels/store" "github.com/mattermost/mattermost/server/v8/channels/store/sqlstore" "github.com/mattermost/mattermost/server/v8/channels/store/storetest/mocks" diff --git a/server/channels/app/import.go b/server/channels/app/import.go index ceb8ca93edf..c877581f52f 100644 --- a/server/channels/app/import.go +++ b/server/channels/app/import.go @@ -16,8 +16,8 @@ import ( "github.com/mattermost/mattermost/server/public/model" "github.com/mattermost/mattermost/server/public/shared/mlog" + "github.com/mattermost/mattermost/server/public/shared/request" "github.com/mattermost/mattermost/server/v8/channels/app/imports" - "github.com/mattermost/mattermost/server/v8/channels/app/request" ) type ReactionImportData = imports.ReactionImportData // part of the app interface diff --git a/server/channels/app/import_functions.go b/server/channels/app/import_functions.go index ae2de145979..c8619e9219b 100644 --- a/server/channels/app/import_functions.go +++ b/server/channels/app/import_functions.go @@ -19,8 +19,8 @@ import ( "github.com/mattermost/mattermost/server/public/model" "github.com/mattermost/mattermost/server/public/shared/mlog" + "github.com/mattermost/mattermost/server/public/shared/request" "github.com/mattermost/mattermost/server/v8/channels/app/imports" - "github.com/mattermost/mattermost/server/v8/channels/app/request" "github.com/mattermost/mattermost/server/v8/channels/app/teams" "github.com/mattermost/mattermost/server/v8/channels/app/users" "github.com/mattermost/mattermost/server/v8/channels/store" @@ -275,7 +275,7 @@ func (a *App) importChannel(c request.CTX, data *imports.ChannelImportData, dryR } var channel *model.Channel - if result, err := a.Srv().Store().Channel().GetByNameIncludeDeleted(team.Id, *data.Name, true); err == nil { + if result, gErr := a.Srv().Store().Channel().GetByNameIncludeDeleted(team.Id, *data.Name, true); gErr == nil { channel = result } else { channel = &model.Channel{} @@ -311,13 +311,20 @@ func (a *App) importChannel(c request.CTX, data *imports.ChannelImportData, dryR channel.SchemeId = &scheme.Id } + var chErr *model.AppError if channel.Id == "" { - if _, err := a.CreateChannel(c, channel, false); err != nil { - return err + if _, chErr = a.CreateChannel(c, channel, false); chErr != nil { + return chErr } } else { - if _, err := a.UpdateChannel(c, channel); err != nil { - return err + if _, chErr = a.UpdateChannel(c, channel); chErr != nil { + return chErr + } + } + + if data.DeletedAt != nil && *data.DeletedAt > 0 { + if err := a.Srv().Store().Channel().Delete(channel.Id, *data.DeletedAt); err != nil { + return model.NewAppError("BulkImport", "app.import.import_channel.deleting.app_error", nil, "", http.StatusInternalServerError).Wrap(err) } } @@ -1374,7 +1381,7 @@ func (a *App) getTeamsByNames(names []string) (map[string]*model.Team, *model.Ap } func (a *App) getChannelsByNames(names []string, teamID string) (map[string]*model.Channel, *model.AppError) { - allChannels, err := a.Srv().Store().Channel().GetByNames(teamID, names, true) + allChannels, err := a.Srv().Store().Channel().GetByNamesIncludeDeleted(teamID, names, true) if err != nil { return nil, model.NewAppError("BulkImport", "app.import.get_teams_by_names.some_teams_not_found.error", nil, "", http.StatusBadRequest).Wrap(err) } @@ -1946,7 +1953,7 @@ func (a *App) importEmoji(c request.CTX, data *imports.EmojiImportData, dryRun b var emoji *model.Emoji - emoji, err := a.Srv().Store().Emoji().GetByName(context.Background(), *data.Name, true) + emoji, err := a.Srv().Store().Emoji().GetByName(c, *data.Name, true) if err != nil { var nfErr *store.ErrNotFound if !errors.As(err, &nfErr) { diff --git a/server/channels/app/import_functions_test.go b/server/channels/app/import_functions_test.go index b897df9f5eb..285c15ca1d5 100644 --- a/server/channels/app/import_functions_test.go +++ b/server/channels/app/import_functions_test.go @@ -704,10 +704,24 @@ func TestImportImportChannel(t *testing.T) { assert.Equal(t, *data.Header, channel.Header) assert.Equal(t, *data.Purpose, channel.Purpose) assert.Equal(t, scheme2.Id, *channel.SchemeId) + + // Do a valid archived channel. + now := model.GetMillis() + data.Name = ptrStr("archivedchannel") + data.DisplayName = ptrStr("Archived Channel") + data.Type = &chanOpen + data.Header = ptrStr("Archived Channel Header") + data.Purpose = ptrStr("Archived Channel Purpose") + data.Scheme = &scheme1.Name + data.DeletedAt = &now + err = th.App.importChannel(th.Context, &data, false) + require.Nil(t, err, "Expected success in apply mode") + aChan, err := th.App.GetChannelByName(th.Context, *data.Name, team.Id, true) + require.Nil(t, err, "Failed to get channel from database.") + assert.Equal(t, *data.Name, aChan.Name) } func TestImportImportUser(t *testing.T) { - t.Skip("MM-43341") th := Setup(t) defer th.TearDown() @@ -4154,7 +4168,7 @@ func TestImportImportEmoji(t *testing.T) { appErr := th.App.importEmoji(th.Context, &data, true) assert.NotNil(t, appErr, "Invalid emoji should have failed dry run") - emoji, nErr := th.App.Srv().Store().Emoji().GetByName(context.Background(), *data.Name, true) + emoji, nErr := th.App.Srv().Store().Emoji().GetByName(th.Context, *data.Name, true) assert.Nil(t, emoji, "Emoji should not have been imported") assert.Error(t, nErr) @@ -4174,7 +4188,7 @@ func TestImportImportEmoji(t *testing.T) { appErr = th.App.importEmoji(th.Context, &data, false) assert.Nil(t, appErr, "Valid emoji should have succeeded apply mode") - emoji, nErr = th.App.Srv().Store().Emoji().GetByName(context.Background(), *data.Name, true) + emoji, nErr = th.App.Srv().Store().Emoji().GetByName(th.Context, *data.Name, true) assert.NotNil(t, emoji, "Emoji should have been imported") assert.NoError(t, nErr, "Emoji should have been imported without any error") @@ -4189,7 +4203,7 @@ func TestImportImportEmoji(t *testing.T) { data = imports.EmojiImportData{Name: ptrStr(model.NewId()), Image: ptrStr(largeImage)} appErr = th.App.importEmoji(th.Context, &data, false) require.NotNil(t, appErr) - require.ErrorIs(t, appErr.Unwrap(), utils.SizeLimitExceeded) + require.ErrorIs(t, appErr.Unwrap(), utils.ErrSizeLimitExceeded) } func TestImportAttachment(t *testing.T) { diff --git a/server/channels/app/import_test.go b/server/channels/app/import_test.go index b57c4a2c033..df209fb8e04 100644 --- a/server/channels/app/import_test.go +++ b/server/channels/app/import_test.go @@ -18,8 +18,8 @@ import ( "github.com/mattermost/mattermost/server/public/model" "github.com/mattermost/mattermost/server/public/shared/mlog" + "github.com/mattermost/mattermost/server/public/shared/request" "github.com/mattermost/mattermost/server/v8/channels/app/imports" - "github.com/mattermost/mattermost/server/v8/channels/app/request" "github.com/mattermost/mattermost/server/v8/channels/utils" "github.com/mattermost/mattermost/server/v8/channels/utils/fileutils" ) @@ -287,8 +287,7 @@ func AssertFileIdsInPost(files []*model.FileInfo, th *TestHelper, t *testing.T) } func TestProcessAttachments(t *testing.T) { - logger, _ := mlog.NewLogger() - c := request.EmptyContext(logger) + c := request.EmptyContext(mlog.CreateConsoleTestLogger(t)) genAttachments := func() *[]imports.AttachmentImportData { return &[]imports.AttachmentImportData{ diff --git a/server/channels/app/imports/import_types.go b/server/channels/app/imports/import_types.go index 785cd8ad1cc..1e4caf1ae0f 100644 --- a/server/channels/app/imports/import_types.go +++ b/server/channels/app/imports/import_types.go @@ -50,6 +50,7 @@ type ChannelImportData struct { Header *string `json:"header,omitempty"` Purpose *string `json:"purpose,omitempty"` Scheme *string `json:"scheme,omitempty"` + DeletedAt *int64 `json:"deleted_at,omitempty"` } type UserImportData struct { diff --git a/server/channels/app/integration_action.go b/server/channels/app/integration_action.go index ca9c64f3e4c..625055a0471 100644 --- a/server/channels/app/integration_action.go +++ b/server/channels/app/integration_action.go @@ -35,7 +35,7 @@ import ( "github.com/mattermost/mattermost/server/public/model" "github.com/mattermost/mattermost/server/public/shared/i18n" "github.com/mattermost/mattermost/server/public/shared/mlog" - "github.com/mattermost/mattermost/server/v8/channels/app/request" + "github.com/mattermost/mattermost/server/public/shared/request" "github.com/mattermost/mattermost/server/v8/channels/store" "github.com/mattermost/mattermost/server/v8/channels/utils" ) diff --git a/server/channels/app/job.go b/server/channels/app/job.go index 085ce57388b..48e253d2d7d 100644 --- a/server/channels/app/job.go +++ b/server/channels/app/job.go @@ -8,11 +8,12 @@ import ( "net/http" "github.com/mattermost/mattermost/server/public/model" + "github.com/mattermost/mattermost/server/public/shared/request" "github.com/mattermost/mattermost/server/v8/channels/store" ) -func (a *App) GetJob(id string) (*model.Job, *model.AppError) { - job, err := a.Srv().Store().Job().Get(id) +func (a *App) GetJob(c *request.Context, id string) (*model.Job, *model.AppError) { + job, err := a.Srv().Store().Job().Get(c, id) if err != nil { var nfErr *store.ErrNotFound switch { @@ -26,25 +27,12 @@ func (a *App) GetJob(id string) (*model.Job, *model.AppError) { return job, nil } -func (a *App) GetJobsPage(page int, perPage int) ([]*model.Job, *model.AppError) { - return a.GetJobs(page*perPage, perPage) +func (a *App) GetJobsByTypePage(c *request.Context, jobType string, page int, perPage int) ([]*model.Job, *model.AppError) { + return a.GetJobsByType(c, jobType, page*perPage, perPage) } -func (a *App) GetJobs(offset int, limit int) ([]*model.Job, *model.AppError) { - jobs, err := a.Srv().Store().Job().GetAllPage(offset, limit) - if err != nil { - return nil, model.NewAppError("GetJobs", "app.job.get_all.app_error", nil, "", http.StatusInternalServerError).Wrap(err) - } - - return jobs, nil -} - -func (a *App) GetJobsByTypePage(jobType string, page int, perPage int) ([]*model.Job, *model.AppError) { - return a.GetJobsByType(jobType, page*perPage, perPage) -} - -func (a *App) GetJobsByType(jobType string, offset int, limit int) ([]*model.Job, *model.AppError) { - jobs, err := a.Srv().Store().Job().GetAllByTypePage(jobType, offset, limit) +func (a *App) GetJobsByType(c *request.Context, jobType string, offset int, limit int) ([]*model.Job, *model.AppError) { + jobs, err := a.Srv().Store().Job().GetAllByTypePage(c, jobType, offset, limit) if err != nil { return nil, model.NewAppError("GetJobsByType", "app.job.get_all.app_error", nil, "", http.StatusInternalServerError).Wrap(err) } @@ -52,24 +40,24 @@ func (a *App) GetJobsByType(jobType string, offset int, limit int) ([]*model.Job return jobs, nil } -func (a *App) GetJobsByTypesPage(jobType []string, page int, perPage int) ([]*model.Job, *model.AppError) { - return a.GetJobsByTypes(jobType, page*perPage, perPage) +func (a *App) GetJobsByTypesPage(c *request.Context, jobType []string, page int, perPage int) ([]*model.Job, *model.AppError) { + return a.GetJobsByTypes(c, jobType, page*perPage, perPage) } -func (a *App) GetJobsByTypes(jobTypes []string, offset int, limit int) ([]*model.Job, *model.AppError) { - jobs, err := a.Srv().Store().Job().GetAllByTypesPage(jobTypes, offset, limit) +func (a *App) GetJobsByTypes(c *request.Context, jobTypes []string, offset int, limit int) ([]*model.Job, *model.AppError) { + jobs, err := a.Srv().Store().Job().GetAllByTypesPage(c, jobTypes, offset, limit) if err != nil { return nil, model.NewAppError("GetJobsByType", "app.job.get_all.app_error", nil, "", http.StatusInternalServerError).Wrap(err) } return jobs, nil } -func (a *App) CreateJob(job *model.Job) (*model.Job, *model.AppError) { - return a.Srv().Jobs.CreateJob(job.Type, job.Data) +func (a *App) CreateJob(c *request.Context, job *model.Job) (*model.Job, *model.AppError) { + return a.Srv().Jobs.CreateJob(c, job.Type, job.Data) } -func (a *App) CancelJob(jobId string) *model.AppError { - return a.Srv().Jobs.RequestCancellation(jobId) +func (a *App) CancelJob(c *request.Context, jobId string) *model.AppError { + return a.Srv().Jobs.RequestCancellation(c, jobId) } func (a *App) SessionHasPermissionToCreateJob(session model.Session, job *model.Job) (bool, *model.Permission) { diff --git a/server/channels/app/job_test.go b/server/channels/app/job_test.go index 03ac47c6577..c7911a61f8d 100644 --- a/server/channels/app/job_test.go +++ b/server/channels/app/job_test.go @@ -11,23 +11,27 @@ import ( "github.com/stretchr/testify/require" "github.com/mattermost/mattermost/server/public/model" + "github.com/mattermost/mattermost/server/public/shared/request" "github.com/mattermost/mattermost/server/v8/channels/store/sqlstore" ) func TestGetJob(t *testing.T) { th := Setup(t) defer th.TearDown() + ctx := request.EmptyContext(th.TestLogger) status := &model.Job{ Id: model.NewId(), Status: model.NewId(), } + status.InitLogger(th.TestLogger) + _, err := th.App.Srv().Store().Job().Save(status) require.NoError(t, err) defer th.App.Srv().Store().Job().Delete(status.Id) - received, appErr := th.App.GetJob(status.Id) + received, appErr := th.App.GetJob(ctx, status.Id) require.Nil(t, appErr) require.Equal(t, status, received, "incorrect job status received") } @@ -216,6 +220,7 @@ func TestSessionHasPermissionToReadJob(t *testing.T) { func TestGetJobByType(t *testing.T) { th := Setup(t) defer th.TearDown() + ctx := request.EmptyContext(th.TestLogger) jobType := model.NewId() @@ -238,18 +243,20 @@ func TestGetJobByType(t *testing.T) { } for _, status := range statuses { + status.InitLogger(th.TestLogger) + _, err := th.App.Srv().Store().Job().Save(status) require.NoError(t, err) defer th.App.Srv().Store().Job().Delete(status.Id) } - received, err := th.App.GetJobsByType(jobType, 0, 2) + received, err := th.App.GetJobsByType(ctx, jobType, 0, 2) require.Nil(t, err) require.Len(t, received, 2, "received wrong number of statuses") require.Equal(t, statuses[2], received[0], "should've received newest job first") require.Equal(t, statuses[0], received[1], "should've received second newest job second") - received, err = th.App.GetJobsByType(jobType, 2, 2) + received, err = th.App.GetJobsByType(ctx, jobType, 2, 2) require.Nil(t, err) require.Len(t, received, 1, "received wrong number of statuses") require.Equal(t, statuses[1], received[0], "should've received oldest job last") @@ -258,6 +265,7 @@ func TestGetJobByType(t *testing.T) { func TestGetJobsByTypes(t *testing.T) { th := Setup(t) defer th.TearDown() + ctx := request.EmptyContext(th.TestLogger) jobType := model.NewId() jobType1 := model.NewId() @@ -282,25 +290,27 @@ func TestGetJobsByTypes(t *testing.T) { } for _, status := range statuses { + status.InitLogger(th.TestLogger) + _, err := th.App.Srv().Store().Job().Save(status) require.NoError(t, err) defer th.App.Srv().Store().Job().Delete(status.Id) } jobTypes := []string{jobType, jobType1, jobType2} - received, err := th.App.GetJobsByTypes(jobTypes, 0, 2) + received, err := th.App.GetJobsByTypes(ctx, jobTypes, 0, 2) require.Nil(t, err) require.Len(t, received, 2, "received wrong number of jobs") require.Equal(t, statuses[2], received[0], "should've received newest job first") require.Equal(t, statuses[0], received[1], "should've received second newest job second") - received, err = th.App.GetJobsByTypes(jobTypes, 2, 2) + received, err = th.App.GetJobsByTypes(ctx, jobTypes, 2, 2) require.Nil(t, err) require.Len(t, received, 1, "received wrong number of jobs") require.Equal(t, statuses[1], received[0], "should've received oldest job last") jobTypes = []string{jobType1, jobType2} - received, err = th.App.GetJobsByTypes(jobTypes, 0, 3) + received, err = th.App.GetJobsByTypes(ctx, jobTypes, 0, 3) require.Nil(t, err) require.Len(t, received, 2, "received wrong number of jobs") require.Equal(t, statuses[2], received[0], "received wrong job type") diff --git a/server/channels/app/layer_generators/opentracing_layer.go.tmpl b/server/channels/app/layer_generators/opentracing_layer.go.tmpl index 7b71bfee5f3..494b27feaf7 100644 --- a/server/channels/app/layer_generators/opentracing_layer.go.tmpl +++ b/server/channels/app/layer_generators/opentracing_layer.go.tmpl @@ -8,6 +8,7 @@ package opentracing import ( "github.com/opentracing/opentracing-go/ext" + spanlog "github.com/opentracing/opentracing-go/log" ) @@ -140,4 +141,4 @@ func (a *{{.Name}}) Timezones() *timezones.Timezones { } func (a *{{.Name}}) SetServer(srv *app.Server) { a.srv = srv -} \ No newline at end of file +} diff --git a/server/channels/app/ldap.go b/server/channels/app/ldap.go index 9ad70447b80..74f27ce1fac 100644 --- a/server/channels/app/ldap.go +++ b/server/channels/app/ldap.go @@ -11,12 +11,13 @@ import ( "github.com/mattermost/mattermost/server/public/model" "github.com/mattermost/mattermost/server/public/shared/i18n" "github.com/mattermost/mattermost/server/public/shared/mlog" + "github.com/mattermost/mattermost/server/public/shared/request" ) // SyncLdap starts an LDAP sync job. // If includeRemovedMembers is true, then members who left or were removed from a team/channel will // be re-added; otherwise, they will not be re-added. -func (a *App) SyncLdap(includeRemovedMembers bool) { +func (a *App) SyncLdap(c *request.Context, includeRemovedMembers bool) { a.Srv().Go(func() { if license := a.Srv().License(); license != nil && *license.Features.LDAP { @@ -30,7 +31,7 @@ func (a *App) SyncLdap(includeRemovedMembers bool) { mlog.Error("Not executing ldap sync because ldap is not available") return } - ldapI.StartSynchronizeJob(false, includeRemovedMembers) + ldapI.StartSynchronizeJob(c, false, includeRemovedMembers) } }) } diff --git a/server/channels/app/login.go b/server/channels/app/login.go index d7a97a68f47..fec7bb2425a 100644 --- a/server/channels/app/login.go +++ b/server/channels/app/login.go @@ -19,7 +19,7 @@ import ( "github.com/mattermost/mattermost/server/public/model" "github.com/mattermost/mattermost/server/public/plugin" "github.com/mattermost/mattermost/server/public/shared/mlog" - "github.com/mattermost/mattermost/server/v8/channels/app/request" + "github.com/mattermost/mattermost/server/public/shared/request" "github.com/mattermost/mattermost/server/v8/channels/store" "github.com/mattermost/mattermost/server/v8/channels/utils" ) diff --git a/server/channels/app/migrations.go b/server/channels/app/migrations.go index 0ddf32f0b71..74860c84ef3 100644 --- a/server/channels/app/migrations.go +++ b/server/channels/app/migrations.go @@ -11,6 +11,7 @@ import ( "github.com/mattermost/mattermost/server/public/model" "github.com/mattermost/mattermost/server/public/shared/mlog" + "github.com/mattermost/mattermost/server/public/shared/request" ) const EmojisPermissionsMigrationKey = "EmojisPermissionsMigrationComplete" @@ -554,7 +555,7 @@ func (s *Server) doPostPriorityConfigDefaultTrueMigration() { } } -func (s *Server) doElasticsearchFixChannelIndex() { +func (s *Server) doElasticsearchFixChannelIndex(c *request.Context) { // If the migration is already marked as completed, don't do it again. if _, err := s.Store().System().GetByName(model.MigrationKeyElasticsearchFixChannelIndex); err == nil { return @@ -566,13 +567,13 @@ func (s *Server) doElasticsearchFixChannelIndex() { return } - if _, appErr := s.Jobs.CreateJob(model.JobTypeElasticsearchFixChannelIndex, nil); appErr != nil { + if _, appErr := s.Jobs.CreateJob(c, model.JobTypeElasticsearchFixChannelIndex, nil); appErr != nil { mlog.Fatal("failed to start job for fixing Elasticsearch channels index", mlog.Err(appErr)) return } } -func (s *Server) doCloudS3PathMigrations() { +func (s *Server) doCloudS3PathMigrations(c *request.Context) { // This migration is only applicable for cloud environments if os.Getenv("MM_CLOUD_FILESTORE_BIFROST") == "" { return @@ -585,7 +586,7 @@ func (s *Server) doCloudS3PathMigrations() { // If there is a job already pending, no need to schedule again. // This is possible if the pod was rolled over. - jobs, err := s.Store().Job().GetAllByTypeAndStatus(model.JobTypeS3PathMigration, model.JobStatusPending) + jobs, err := s.Store().Job().GetAllByTypeAndStatus(c, model.JobTypeS3PathMigration, model.JobStatusPending) if err != nil { mlog.Fatal("failed to get jobs by type and status", mlog.Err(err)) return @@ -594,7 +595,7 @@ func (s *Server) doCloudS3PathMigrations() { return } - if _, appErr := s.Jobs.CreateJobOnce(model.JobTypeS3PathMigration, nil); appErr != nil { + if _, appErr := s.Jobs.CreateJobOnce(c, model.JobTypeS3PathMigration, nil); appErr != nil { mlog.Fatal("failed to start job for migrating s3 file paths", mlog.Err(appErr)) return } @@ -606,6 +607,8 @@ func (a *App) DoAppMigrations() { } func (s *Server) doAppMigrations() { + c := request.EmptyContext(s.Log()) + s.doAdvancedPermissionsMigration() s.doEmojisPermissionsMigration() s.doGuestRolesCreationMigration() @@ -622,6 +625,6 @@ func (s *Server) doAppMigrations() { s.doFirstAdminSetupCompleteMigration() s.doRemainingSchemaMigrations() s.doPostPriorityConfigDefaultTrueMigration() - s.doElasticsearchFixChannelIndex() - s.doCloudS3PathMigrations() + s.doElasticsearchFixChannelIndex(c) + s.doCloudS3PathMigrations(c) } diff --git a/server/channels/app/notification.go b/server/channels/app/notification.go index fa5607c4a0e..976644a134d 100644 --- a/server/channels/app/notification.go +++ b/server/channels/app/notification.go @@ -19,7 +19,7 @@ import ( "github.com/mattermost/mattermost/server/public/shared/i18n" "github.com/mattermost/mattermost/server/public/shared/markdown" "github.com/mattermost/mattermost/server/public/shared/mlog" - "github.com/mattermost/mattermost/server/v8/channels/app/request" + "github.com/mattermost/mattermost/server/public/shared/request" "github.com/mattermost/mattermost/server/v8/channels/store" ) @@ -387,7 +387,9 @@ func (a *App) SendNotifications(c request.CTX, post *model.Post, team *model.Tea status = &model.Status{UserId: id, Status: model.StatusOffline, Manual: false, LastActivityAt: 0, ActiveChannel: ""} } - if ShouldSendPushNotification(profileMap[id], channelMemberNotifyPropsMap[id], true, status, post) { + isExplicitlyMentioned := mentions.Mentions[id] > GMMention + isGM := channel.Type == model.ChannelTypeGroup + if ShouldSendPushNotification(profileMap[id], channelMemberNotifyPropsMap[id], isExplicitlyMentioned, status, post, isGM) { mentionType := mentions.Mentions[id] replyToThreadType := "" @@ -428,7 +430,8 @@ func (a *App) SendNotifications(c request.CTX, post *model.Post, team *model.Tea status = &model.Status{UserId: id, Status: model.StatusOffline, Manual: false, LastActivityAt: 0, ActiveChannel: ""} } - if ShouldSendPushNotification(profileMap[id], channelMemberNotifyPropsMap[id], false, status, post) { + isGM := channel.Type == model.ChannelTypeGroup + if ShouldSendPushNotification(profileMap[id], channelMemberNotifyPropsMap[id], false, status, post, isGM) { a.sendPushNotification( notification, profileMap[id], @@ -770,6 +773,14 @@ func (a *App) getExplicitMentionsAndKeywords(c request.CTX, post *model.Post, ch keywords = a.getMentionKeywordsInChannel(profileMap, allowChannelMentions, channelMemberNotifyPropsMap) mentions = getExplicitMentions(post, keywords, groups) + + // Add a GM mention to all members of a GM channel + if channel.Type == model.ChannelTypeGroup { + for id := range channelMemberNotifyPropsMap { + mentions.addMention(id, GMMention) + } + } + // Add an implicit mention when a user is added to a channel // even if the user has set 'username mentions' to false in account settings. if post.Type == model.PostTypeAddToChannel { @@ -1063,6 +1074,9 @@ const ( // A placeholder that should never be used in practice NoMention MentionType = iota + // The post is in a GM + GMMention + // The post is in a thread that the user has commented on ThreadMention diff --git a/server/channels/app/notification_email.go b/server/channels/app/notification_email.go index 11f2ae2e995..e3df43a41a8 100644 --- a/server/channels/app/notification_email.go +++ b/server/channels/app/notification_email.go @@ -17,8 +17,8 @@ import ( "github.com/mattermost/mattermost/server/public/model" "github.com/mattermost/mattermost/server/public/shared/i18n" "github.com/mattermost/mattermost/server/public/shared/mlog" + "github.com/mattermost/mattermost/server/public/shared/request" email "github.com/mattermost/mattermost/server/v8/channels/app/email" - "github.com/mattermost/mattermost/server/v8/channels/app/request" "github.com/mattermost/mattermost/server/v8/channels/utils" ) diff --git a/server/channels/app/notification_push.go b/server/channels/app/notification_push.go index fa05738a70f..a928f743768 100644 --- a/server/channels/app/notification_push.go +++ b/server/channels/app/notification_push.go @@ -14,10 +14,12 @@ import ( "strings" "sync" + "github.com/mattermost/mattermost/server/public/plugin" + "github.com/mattermost/mattermost/server/public/model" "github.com/mattermost/mattermost/server/public/shared/i18n" "github.com/mattermost/mattermost/server/public/shared/mlog" - "github.com/mattermost/mattermost/server/v8/channels/app/request" + "github.com/mattermost/mattermost/server/public/shared/request" "github.com/mattermost/mattermost/server/v8/channels/utils" ) @@ -79,6 +81,25 @@ func (a *App) sendPushNotificationSync(c request.CTX, post *model.Post, user *mo } func (a *App) sendPushNotificationToAllSessions(msg *model.PushNotification, userID string, skipSessionId string) *model.AppError { + rejectionReason := "" + a.ch.RunMultiHook(func(hooks plugin.Hooks) bool { + var replacementNotification *model.PushNotification + replacementNotification, rejectionReason = hooks.NotificationWillBePushed(msg, userID) + if rejectionReason != "" { + mlog.Info("Notification cancelled by plugin.", mlog.String("rejection reason", rejectionReason)) + return false + } + if replacementNotification != nil { + msg = replacementNotification + } + return true + }, plugin.NotificationWillBePushedID) + + if rejectionReason != "" { + // Notifications rejected by a plugin should not be considered errors + return nil + } + sessions, err := a.getMobileAppSessions(userID) if err != nil { return err @@ -491,12 +512,12 @@ func (a *App) getMobileAppSessions(userID string) ([]*model.Session, *model.AppE return sessions, nil } -func ShouldSendPushNotification(user *model.User, channelNotifyProps model.StringMap, wasMentioned bool, status *model.Status, post *model.Post) bool { - return DoesNotifyPropsAllowPushNotification(user, channelNotifyProps, post, wasMentioned) && +func ShouldSendPushNotification(user *model.User, channelNotifyProps model.StringMap, wasMentioned bool, status *model.Status, post *model.Post, isGM bool) bool { + return DoesNotifyPropsAllowPushNotification(user, channelNotifyProps, post, wasMentioned, isGM) && DoesStatusAllowPushNotification(user.NotifyProps, status, post.ChannelId) } -func DoesNotifyPropsAllowPushNotification(user *model.User, channelNotifyProps model.StringMap, post *model.Post, wasMentioned bool) bool { +func DoesNotifyPropsAllowPushNotification(user *model.User, channelNotifyProps model.StringMap, post *model.Post, wasMentioned, isGM bool) bool { userNotifyProps := user.NotifyProps userNotify := userNotifyProps[model.PushNotifyProp] channelNotify, ok := channelNotifyProps[model.PushNotifyProp] @@ -504,6 +525,14 @@ func DoesNotifyPropsAllowPushNotification(user *model.User, channelNotifyProps m channelNotify = model.ChannelNotifyDefault } + notify := channelNotify + if channelNotify == model.ChannelNotifyDefault { + notify = userNotify + if isGM && userNotify == model.UserNotifyMention { + notify = model.ChannelNotifyAll + } + } + // If the channel is muted do not send push notifications if channelNotifyProps[model.MarkUnreadNotifyProp] == model.ChannelMarkUnreadMention { return false @@ -513,28 +542,19 @@ func DoesNotifyPropsAllowPushNotification(user *model.User, channelNotifyProps m return false } - if channelNotify == model.UserNotifyNone { + if notify == model.ChannelNotifyNone { return false } - if channelNotify == model.ChannelNotifyMention && !wasMentioned { + if notify == model.ChannelNotifyMention && !wasMentioned { return false } - if userNotify == model.UserNotifyMention && channelNotify == model.ChannelNotifyDefault && !wasMentioned { - return false - } - - if (userNotify == model.UserNotifyAll || channelNotify == model.ChannelNotifyAll) && + if (notify == model.ChannelNotifyAll) && (post.UserId != user.Id || post.GetProp("from_webhook") == "true") { return true } - if userNotify == model.UserNotifyNone && - channelNotify == model.ChannelNotifyDefault { - return false - } - return true } @@ -583,6 +603,10 @@ func (a *App) BuildPushNotificationMessage(c request.CTX, contentsConfig string, msg.Badge = badgeCount + // Add post and channel types for plugins to use in the NotificationWillBePushed hook + msg.PostType = post.Type + msg.ChannelType = channel.Type + return msg, nil } diff --git a/server/channels/app/notification_push_test.go b/server/channels/app/notification_push_test.go index 0fd3e8848e3..dff3655099d 100644 --- a/server/channels/app/notification_push_test.go +++ b/server/channels/app/notification_push_test.go @@ -36,6 +36,7 @@ func TestDoesNotifyPropsAllowPushNotification(t *testing.T) { wasMentioned bool isMuted bool expected bool + isGM bool }{ { name: "When post is a System Message and has no mentions", @@ -45,6 +46,7 @@ func TestDoesNotifyPropsAllowPushNotification(t *testing.T) { wasMentioned: false, isMuted: false, expected: false, + isGM: false, }, { name: "When post is a System Message and has mentions", @@ -54,6 +56,7 @@ func TestDoesNotifyPropsAllowPushNotification(t *testing.T) { wasMentioned: true, isMuted: false, expected: false, + isGM: false, }, { name: "When default is ALL, no channel props is set and has no mentions", @@ -63,6 +66,7 @@ func TestDoesNotifyPropsAllowPushNotification(t *testing.T) { wasMentioned: false, isMuted: false, expected: true, + isGM: false, }, { name: "When default is ALL, no channel props is set and has mentions", @@ -72,6 +76,7 @@ func TestDoesNotifyPropsAllowPushNotification(t *testing.T) { wasMentioned: true, isMuted: false, expected: true, + isGM: false, }, { name: "When default is MENTION, no channel props is set and has no mentions", @@ -81,6 +86,7 @@ func TestDoesNotifyPropsAllowPushNotification(t *testing.T) { wasMentioned: false, isMuted: false, expected: false, + isGM: false, }, { name: "When default is MENTION, no channel props is set and has mentions", @@ -90,6 +96,7 @@ func TestDoesNotifyPropsAllowPushNotification(t *testing.T) { wasMentioned: true, isMuted: false, expected: true, + isGM: false, }, { name: "When default is NONE, no channel props is set and has no mentions", @@ -99,6 +106,7 @@ func TestDoesNotifyPropsAllowPushNotification(t *testing.T) { wasMentioned: false, isMuted: false, expected: false, + isGM: false, }, { name: "When default is NONE, no channel props is set and has mentions", @@ -108,6 +116,7 @@ func TestDoesNotifyPropsAllowPushNotification(t *testing.T) { wasMentioned: true, isMuted: false, expected: false, + isGM: false, }, { name: "When default is ALL, channel is DEFAULT and has no mentions", @@ -117,6 +126,7 @@ func TestDoesNotifyPropsAllowPushNotification(t *testing.T) { wasMentioned: false, isMuted: false, expected: true, + isGM: false, }, { name: "When default is ALL, channel is DEFAULT and has mentions", @@ -126,6 +136,7 @@ func TestDoesNotifyPropsAllowPushNotification(t *testing.T) { wasMentioned: true, isMuted: false, expected: true, + isGM: false, }, { name: "When default is MENTION, channel is DEFAULT and has no mentions", @@ -135,6 +146,7 @@ func TestDoesNotifyPropsAllowPushNotification(t *testing.T) { wasMentioned: false, isMuted: false, expected: false, + isGM: false, }, { name: "When default is MENTION, channel is DEFAULT and has mentions", @@ -144,6 +156,7 @@ func TestDoesNotifyPropsAllowPushNotification(t *testing.T) { wasMentioned: true, isMuted: false, expected: true, + isGM: false, }, { name: "When default is NONE, channel is DEFAULT and has no mentions", @@ -153,6 +166,7 @@ func TestDoesNotifyPropsAllowPushNotification(t *testing.T) { wasMentioned: false, isMuted: false, expected: false, + isGM: false, }, { name: "When default is NONE, channel is DEFAULT and has mentions", @@ -162,6 +176,7 @@ func TestDoesNotifyPropsAllowPushNotification(t *testing.T) { wasMentioned: true, isMuted: false, expected: false, + isGM: false, }, { name: "When default is ALL, channel is ALL and has no mentions", @@ -171,6 +186,7 @@ func TestDoesNotifyPropsAllowPushNotification(t *testing.T) { wasMentioned: false, isMuted: false, expected: true, + isGM: false, }, { name: "When default is ALL, channel is ALL and has mentions", @@ -180,6 +196,7 @@ func TestDoesNotifyPropsAllowPushNotification(t *testing.T) { wasMentioned: true, isMuted: false, expected: true, + isGM: false, }, { name: "When default is MENTION, channel is ALL and has no mentions", @@ -189,6 +206,7 @@ func TestDoesNotifyPropsAllowPushNotification(t *testing.T) { wasMentioned: false, isMuted: false, expected: true, + isGM: false, }, { name: "When default is MENTION, channel is ALL and has mentions", @@ -198,6 +216,7 @@ func TestDoesNotifyPropsAllowPushNotification(t *testing.T) { wasMentioned: true, isMuted: false, expected: true, + isGM: false, }, { name: "When default is NONE, channel is ALL and has no mentions", @@ -207,6 +226,7 @@ func TestDoesNotifyPropsAllowPushNotification(t *testing.T) { wasMentioned: false, isMuted: false, expected: true, + isGM: false, }, { name: "When default is NONE, channel is ALL and has mentions", @@ -216,6 +236,7 @@ func TestDoesNotifyPropsAllowPushNotification(t *testing.T) { wasMentioned: true, isMuted: false, expected: true, + isGM: false, }, { name: "When default is ALL, channel is MENTION and has no mentions", @@ -225,6 +246,7 @@ func TestDoesNotifyPropsAllowPushNotification(t *testing.T) { wasMentioned: false, isMuted: false, expected: false, + isGM: false, }, { name: "When default is ALL, channel is MENTION and has mentions", @@ -234,6 +256,7 @@ func TestDoesNotifyPropsAllowPushNotification(t *testing.T) { wasMentioned: true, isMuted: false, expected: true, + isGM: false, }, { name: "When default is MENTION, channel is MENTION and has no mentions", @@ -243,6 +266,7 @@ func TestDoesNotifyPropsAllowPushNotification(t *testing.T) { wasMentioned: false, isMuted: false, expected: false, + isGM: false, }, { name: "When default is MENTION, channel is MENTION and has mentions", @@ -252,6 +276,7 @@ func TestDoesNotifyPropsAllowPushNotification(t *testing.T) { wasMentioned: true, isMuted: false, expected: true, + isGM: false, }, { name: "When default is NONE, channel is MENTION and has no mentions", @@ -261,6 +286,7 @@ func TestDoesNotifyPropsAllowPushNotification(t *testing.T) { wasMentioned: false, isMuted: false, expected: false, + isGM: false, }, { name: "When default is NONE, channel is MENTION and has mentions", @@ -270,6 +296,7 @@ func TestDoesNotifyPropsAllowPushNotification(t *testing.T) { wasMentioned: true, isMuted: false, expected: true, + isGM: false, }, { name: "When default is ALL, channel is NONE and has no mentions", @@ -279,6 +306,7 @@ func TestDoesNotifyPropsAllowPushNotification(t *testing.T) { wasMentioned: false, isMuted: false, expected: false, + isGM: false, }, { name: "When default is ALL, channel is NONE and has mentions", @@ -288,6 +316,7 @@ func TestDoesNotifyPropsAllowPushNotification(t *testing.T) { wasMentioned: true, isMuted: false, expected: false, + isGM: false, }, { name: "When default is MENTION, channel is NONE and has no mentions", @@ -297,6 +326,7 @@ func TestDoesNotifyPropsAllowPushNotification(t *testing.T) { wasMentioned: false, isMuted: false, expected: false, + isGM: false, }, { name: "When default is MENTION, channel is NONE and has mentions", @@ -306,6 +336,7 @@ func TestDoesNotifyPropsAllowPushNotification(t *testing.T) { wasMentioned: true, isMuted: false, expected: false, + isGM: false, }, { name: "When default is NONE, channel is NONE and has no mentions", @@ -315,6 +346,7 @@ func TestDoesNotifyPropsAllowPushNotification(t *testing.T) { wasMentioned: false, isMuted: false, expected: false, + isGM: false, }, { name: "When default is NONE, channel is NONE and has mentions", @@ -324,6 +356,7 @@ func TestDoesNotifyPropsAllowPushNotification(t *testing.T) { wasMentioned: true, isMuted: false, expected: false, + isGM: false, }, { name: "When default is ALL, and channel is MUTED", @@ -333,6 +366,47 @@ func TestDoesNotifyPropsAllowPushNotification(t *testing.T) { wasMentioned: false, isMuted: true, expected: false, + isGM: false, + }, + { + name: "For GM default for NONE is NONE", + userNotifySetting: model.UserNotifyNone, + channelNotifySetting: model.ChannelNotifyDefault, + withSystemPost: false, + wasMentioned: false, + isMuted: false, + expected: false, + isGM: true, + }, + { + name: "For GM, mentioned is only called if explicitly mentioned", + userNotifySetting: model.UserNotifyNone, + channelNotifySetting: model.ChannelNotifyMention, + withSystemPost: false, + wasMentioned: true, + isMuted: false, + expected: true, + isGM: true, + }, + { + name: "For GM default for MENTION is ALL", + userNotifySetting: model.UserNotifyMention, + channelNotifySetting: model.ChannelNotifyDefault, + withSystemPost: false, + wasMentioned: false, + isMuted: false, + expected: true, + isGM: true, + }, + { + name: "For GM, mentioned is only called if explicitly mentioned", + userNotifySetting: model.UserNotifyNone, + channelNotifySetting: model.ChannelNotifyMention, + withSystemPost: false, + wasMentioned: false, + isMuted: false, + expected: false, + isGM: true, }, } @@ -352,7 +426,7 @@ func TestDoesNotifyPropsAllowPushNotification(t *testing.T) { if tc.isMuted { channelNotifyProps[model.MarkUnreadNotifyProp] = model.ChannelMarkUnreadMention } - assert.Equal(t, tc.expected, DoesNotifyPropsAllowPushNotification(user, channelNotifyProps, post, tc.wasMentioned)) + assert.Equal(t, tc.expected, DoesNotifyPropsAllowPushNotification(user, channelNotifyProps, post, tc.wasMentioned, tc.isGM)) }) } } diff --git a/server/channels/app/notification_test.go b/server/channels/app/notification_test.go index 9c4b6efc1ee..983f4a4b004 100644 --- a/server/channels/app/notification_test.go +++ b/server/channels/app/notification_test.go @@ -13,8 +13,9 @@ import ( "github.com/mattermost/mattermost/server/public/model" "github.com/mattermost/mattermost/server/public/shared/i18n" + pUtils "github.com/mattermost/mattermost/server/public/utils" + "github.com/mattermost/mattermost/server/v8/channels/store" - "github.com/mattermost/mattermost/server/v8/channels/utils" ) func getLicWithSkuShortName(skuShortName string) *model.License { @@ -37,19 +38,21 @@ func TestSendNotifications(t *testing.T) { th.App.AddUserToChannel(th.Context, th.BasicUser2, th.BasicChannel, false) - post1, appErr := th.App.CreatePostMissingChannel(th.Context, &model.Post{ + post1, createPostErr := th.App.CreatePostMissingChannel(th.Context, &model.Post{ UserId: th.BasicUser.Id, ChannelId: th.BasicChannel.Id, Message: "@" + th.BasicUser2.Username, Type: model.PostTypeAddToChannel, Props: map[string]any{model.PostPropsAddedUserId: "junk"}, }, true, true) - require.Nil(t, appErr) + require.Nil(t, createPostErr) - mentions, err := th.App.SendNotifications(th.Context, post1, th.BasicTeam, th.BasicChannel, th.BasicUser, nil, true) - require.NoError(t, err) - require.NotNil(t, mentions) - require.True(t, utils.StringInSlice(th.BasicUser2.Id, mentions), "mentions", mentions) + t.Run("Basic channel", func(t *testing.T) { + mentions, err := th.App.SendNotifications(th.Context, post1, th.BasicTeam, th.BasicChannel, th.BasicUser, nil, true) + require.NoError(t, err) + require.NotNil(t, mentions) + require.True(t, pUtils.Contains(mentions, th.BasicUser2.Id), "mentions", mentions) + }) t.Run("license is required for group mention", func(t *testing.T) { group := th.CreateGroup() @@ -69,7 +72,7 @@ func TestSendNotifications(t *testing.T) { groupMentionPost, createPostErr := th.App.CreatePost(th.Context, groupMentionPost, th.BasicChannel, false, true) require.Nil(t, createPostErr) - mentions, err = th.App.SendNotifications(th.Context, groupMentionPost, th.BasicTeam, th.BasicChannel, th.BasicUser, nil, true) + mentions, err := th.App.SendNotifications(th.Context, groupMentionPost, th.BasicTeam, th.BasicChannel, th.BasicUser, nil, true) require.NoError(t, err) require.NotNil(t, mentions) require.Len(t, mentions, 0) @@ -82,40 +85,83 @@ func TestSendNotifications(t *testing.T) { require.Len(t, mentions, 1) }) - dm, appErr := th.App.GetOrCreateDirectChannel(th.Context, th.BasicUser.Id, th.BasicUser2.Id) - require.Nil(t, appErr) + t.Run("message in DM generate mention", func(t *testing.T) { + dm, appErr := th.App.GetOrCreateDirectChannel(th.Context, th.BasicUser.Id, th.BasicUser2.Id) + require.Nil(t, appErr) - post2, appErr := th.App.CreatePostMissingChannel(th.Context, &model.Post{ - UserId: th.BasicUser.Id, - ChannelId: dm.Id, - Message: "dm message", - }, true, true) - require.Nil(t, appErr) + post2, appErr := th.App.CreatePostMissingChannel(th.Context, &model.Post{ + UserId: th.BasicUser.Id, + ChannelId: dm.Id, + Message: "dm message", + }, true, true) + require.Nil(t, appErr) - mentions, err = th.App.SendNotifications(th.Context, post2, th.BasicTeam, dm, th.BasicUser, nil, true) - require.NoError(t, err) - require.NotNil(t, mentions) + mentions, err := th.App.SendNotifications(th.Context, post2, th.BasicTeam, dm, th.BasicUser, nil, true) + require.NoError(t, err) + require.NotNil(t, mentions) - _, appErr = th.App.UpdateActive(th.Context, th.BasicUser2, false) - require.Nil(t, appErr) - appErr = th.App.Srv().InvalidateAllCaches() - require.Nil(t, appErr) + _, appErr = th.App.UpdateActive(th.Context, th.BasicUser2, false) + require.Nil(t, appErr) + appErr = th.App.Srv().InvalidateAllCaches() + require.Nil(t, appErr) - post3, appErr := th.App.CreatePostMissingChannel(th.Context, &model.Post{ - UserId: th.BasicUser.Id, - ChannelId: dm.Id, - Message: "dm message", - }, true, true) - require.Nil(t, appErr) + post3, appErr := th.App.CreatePostMissingChannel(th.Context, &model.Post{ + UserId: th.BasicUser.Id, + ChannelId: dm.Id, + Message: "dm message", + }, true, true) + require.Nil(t, appErr) - mentions, err = th.App.SendNotifications(th.Context, post3, th.BasicTeam, dm, th.BasicUser, nil, true) - require.NoError(t, err) - require.NotNil(t, mentions) + mentions, err = th.App.SendNotifications(th.Context, post3, th.BasicTeam, dm, th.BasicUser, nil, true) + require.NoError(t, err) + require.NotNil(t, mentions) - th.BasicChannel.DeleteAt = 1 - mentions, err = th.App.SendNotifications(th.Context, post1, th.BasicTeam, th.BasicChannel, th.BasicUser, nil, true) - require.NoError(t, err) - require.Empty(t, mentions) + th.BasicChannel.DeleteAt = 1 + mentions, err = th.App.SendNotifications(th.Context, post1, th.BasicTeam, th.BasicChannel, th.BasicUser, nil, true) + require.NoError(t, err) + require.Empty(t, mentions) + }) + + t.Run("message in GM generate mention", func(t *testing.T) { + users := []*model.User{} + for i := 0; i < 2; i++ { + user := th.CreateUser() + users = append(users, user) + } + channel := th.CreateGroupChannel(th.Context, users[0], users[1]) + + post2, appErr := th.App.CreatePostMissingChannel(th.Context, &model.Post{ + UserId: users[0].Id, + ChannelId: channel.Id, + Message: "gm message", + }, true, true) + require.Nil(t, appErr) + + mentions, err := th.App.SendNotifications(th.Context, post2, th.BasicTeam, channel, users[0], nil, true) + require.NoError(t, err) + require.NotNil(t, mentions) + + _, appErr = th.App.UpdateActive(th.Context, users[1], false) + require.Nil(t, appErr) + appErr = th.App.Srv().InvalidateAllCaches() + require.Nil(t, appErr) + + post3, appErr := th.App.CreatePostMissingChannel(th.Context, &model.Post{ + UserId: users[0].Id, + ChannelId: channel.Id, + Message: "gm message", + }, true, true) + require.Nil(t, appErr) + + mentions, err = th.App.SendNotifications(th.Context, post3, th.BasicTeam, channel, users[0], nil, true) + require.NoError(t, err) + require.NotNil(t, mentions) + + th.BasicChannel.DeleteAt = 1 + mentions, err = th.App.SendNotifications(th.Context, post1, th.BasicTeam, th.BasicChannel, users[0], nil, true) + require.NoError(t, err) + require.Empty(t, mentions) + }) t.Run("replies to post created by OAuth bot should not notify user", func(t *testing.T) { th := Setup(t).InitBasic() @@ -128,7 +174,7 @@ func TestSendNotifications(t *testing.T) { Props: model.StringInterface{"from_webhook": "true", "override_username": "a bot"}, } - rootPost, appErr = th.App.CreatePostMissingChannel(th.Context, rootPost, false, true) + rootPost, appErr := th.App.CreatePostMissingChannel(th.Context, rootPost, false, true) require.Nil(t, appErr) childPost := &model.Post{ @@ -144,11 +190,12 @@ func TestSendNotifications(t *testing.T) { Order: []string{rootPost.Id, childPost.Id}, Posts: map[string]*model.Post{rootPost.Id: rootPost, childPost.Id: childPost}, } - mentions, err = th.App.SendNotifications(th.Context, childPost, th.BasicTeam, th.BasicChannel, th.BasicUser2, &postList, true) + mentions, err := th.App.SendNotifications(th.Context, childPost, th.BasicTeam, th.BasicChannel, th.BasicUser2, &postList, true) require.NoError(t, err) - require.False(t, utils.StringInSlice(user.Id, mentions)) + require.False(t, pUtils.Contains(mentions, user.Id)) } + var appErr *model.AppError th.BasicUser.NotifyProps[model.CommentsNotifyProp] = model.CommentsNotifyAny th.BasicUser, appErr = th.App.UpdateUser(th.Context, th.BasicUser, false) require.Nil(t, appErr) @@ -2816,7 +2863,7 @@ func TestReplyPostNotificationsWithCRT(t *testing.T) { } mentions, err := th.App.SendNotifications(th.Context, childPost, th.BasicTeam, th.BasicChannel, th.BasicUser2, &postList, true) require.NoError(t, err) - assert.False(t, utils.StringInSlice(user.Id, mentions)) + assert.False(t, pUtils.Contains(mentions, user.Id)) membership, err := th.App.GetThreadMembershipForUser(user.Id, rootPost.Id) assert.Error(t, err) diff --git a/server/channels/app/notify_admin.go b/server/channels/app/notify_admin.go index 54a319cfaf6..5287fa99341 100644 --- a/server/channels/app/notify_admin.go +++ b/server/channels/app/notify_admin.go @@ -14,7 +14,7 @@ import ( "github.com/mattermost/mattermost/server/public/model" "github.com/mattermost/mattermost/server/public/shared/i18n" "github.com/mattermost/mattermost/server/public/shared/mlog" - "github.com/mattermost/mattermost/server/v8/channels/app/request" + "github.com/mattermost/mattermost/server/public/shared/request" "github.com/mattermost/mattermost/server/v8/channels/store" ) diff --git a/server/channels/app/notify_admin_test.go b/server/channels/app/notify_admin_test.go index 2be48e8f5bb..88271554e93 100644 --- a/server/channels/app/notify_admin_test.go +++ b/server/channels/app/notify_admin_test.go @@ -4,7 +4,6 @@ package app import ( - "context" "fmt" "os" "testing" @@ -13,7 +12,7 @@ import ( "github.com/stretchr/testify/require" "github.com/mattermost/mattermost/server/public/model" - "github.com/mattermost/mattermost/server/v8/channels/app/request" + "github.com/mattermost/mattermost/server/public/shared/request" ) const PluginIdGithub = "github" @@ -22,10 +21,10 @@ func Test_SendNotifyAdminPosts(t *testing.T) { t.Run("no error sending non trial upgrade post when no notifications are available", func(t *testing.T) { th := Setup(t).InitBasic() defer th.TearDown() + ctx := request.EmptyContext(th.TestLogger) th.App.Srv().SetLicense(model.NewTestLicense("cloud")) - ctx := request.NewContext(context.Background(), model.NewId(), model.NewId(), model.NewId(), model.NewId(), model.NewId(), model.Session{}, nil) err := th.App.SendNotifyAdminPosts(ctx, "", "", false) require.Nil(t, err) }) @@ -33,10 +32,10 @@ func Test_SendNotifyAdminPosts(t *testing.T) { t.Run("no error sending trial upgrade post when no notifications are available", func(t *testing.T) { th := Setup(t).InitBasic() defer th.TearDown() + ctx := request.EmptyContext(th.TestLogger) th.App.Srv().SetLicense(model.NewTestLicense("cloud")) - ctx := request.NewContext(context.Background(), model.NewId(), model.NewId(), model.NewId(), model.NewId(), model.NewId(), model.Session{}, nil) err := th.App.SendNotifyAdminPosts(ctx, "", "", true) require.Nil(t, err) }) @@ -44,6 +43,7 @@ func Test_SendNotifyAdminPosts(t *testing.T) { t.Run("successfully send upgrade notification", func(t *testing.T) { th := Setup(t).InitBasic() defer th.TearDown() + ctx := request.EmptyContext(th.TestLogger) th.App.Srv().SetLicense(model.NewTestLicense("cloud")) @@ -62,7 +62,6 @@ func Test_SendNotifyAdminPosts(t *testing.T) { }) require.Nil(t, appErr) - ctx := request.NewContext(context.Background(), model.NewId(), model.NewId(), model.NewId(), model.NewId(), model.NewId(), model.Session{}, nil) appErr = th.App.SendNotifyAdminPosts(ctx, "test", "", false) require.Nil(t, appErr) @@ -98,6 +97,7 @@ func Test_SendNotifyAdminPosts(t *testing.T) { t.Run("successfully send trial upgrade notification", func(t *testing.T) { th := Setup(t).InitBasic() defer th.TearDown() + ctx := request.EmptyContext(th.TestLogger) th.App.Srv().SetLicense(model.NewTestLicense("cloud")) @@ -110,7 +110,6 @@ func Test_SendNotifyAdminPosts(t *testing.T) { }) require.Nil(t, appErr) - ctx := request.NewContext(context.Background(), model.NewId(), model.NewId(), model.NewId(), model.NewId(), model.NewId(), model.Session{}, nil) appErr = th.App.SendNotifyAdminPosts(ctx, "test", "", true) require.Nil(t, appErr) @@ -146,6 +145,7 @@ func Test_SendNotifyAdminPosts(t *testing.T) { t.Run("successfully send install plugin notification", func(t *testing.T) { th := Setup(t).InitBasic() defer th.TearDown() + ctx := request.EmptyContext(th.TestLogger) // some notifications _, appErr := th.App.SaveAdminNotifyData(&model.NotifyAdminData{ @@ -156,7 +156,6 @@ func Test_SendNotifyAdminPosts(t *testing.T) { }) require.Nil(t, appErr) - ctx := request.NewContext(context.Background(), model.NewId(), model.NewId(), model.NewId(), model.NewId(), model.NewId(), model.Session{}, nil) appErr = th.App.SendNotifyAdminPosts(ctx, "", "", false) require.Nil(t, appErr) @@ -191,6 +190,7 @@ func Test_SendNotifyAdminPosts(t *testing.T) { t.Run("persist notify admin data after sending the install plugin notification", func(t *testing.T) { th := Setup(t).InitBasic() defer th.TearDown() + ctx := request.EmptyContext(th.TestLogger) // some notifications _, appErr := th.App.SaveAdminNotifyData(&model.NotifyAdminData{ @@ -201,7 +201,6 @@ func Test_SendNotifyAdminPosts(t *testing.T) { }) require.Nil(t, appErr) - ctx := request.NewContext(context.Background(), model.NewId(), model.NewId(), model.NewId(), model.NewId(), model.NewId(), model.Session{}, nil) appErr = th.App.SendNotifyAdminPosts(ctx, "", "", false) require.Nil(t, appErr) @@ -261,6 +260,7 @@ func Test_SendNotifyAdminPosts(t *testing.T) { t.Run("error when trying to send upgrade post before end of cool off period", func(t *testing.T) { th := Setup(t).InitBasic() defer th.TearDown() + ctx := request.EmptyContext(th.TestLogger) th.App.Srv().SetLicense(model.NewTestLicense("cloud")) @@ -272,7 +272,6 @@ func Test_SendNotifyAdminPosts(t *testing.T) { }) require.Nil(t, appErr) - ctx := request.NewContext(context.Background(), model.NewId(), model.NewId(), model.NewId(), model.NewId(), model.NewId(), model.Session{}, nil) appErr = th.App.SendNotifyAdminPosts(ctx, "", "", false) require.Nil(t, appErr) @@ -293,6 +292,7 @@ func Test_SendNotifyAdminPosts(t *testing.T) { t.Run("can send upgrade post at the end of cool off period", func(t *testing.T) { th := Setup(t).InitBasic() defer th.TearDown() + ctx := request.EmptyContext(th.TestLogger) th.App.Srv().SetLicense(model.NewTestLicense("cloud")) @@ -307,7 +307,6 @@ func Test_SendNotifyAdminPosts(t *testing.T) { }) require.Nil(t, appErr) - ctx := request.NewContext(context.Background(), model.NewId(), model.NewId(), model.NewId(), model.NewId(), model.NewId(), model.Session{}, nil) appErr = th.App.SendNotifyAdminPosts(ctx, "", "", false) require.Nil(t, appErr) @@ -329,6 +328,7 @@ func Test_SendNotifyAdminPosts(t *testing.T) { t.Run("can filter notifications when plan changes within cool off period", func(t *testing.T) { th := Setup(t).InitBasic() defer th.TearDown() + ctx := request.EmptyContext(th.TestLogger) th.App.Srv().SetLicense(model.NewTestLicense("cloud")) @@ -349,7 +349,6 @@ func Test_SendNotifyAdminPosts(t *testing.T) { }) require.Nil(t, appErr) - ctx := request.NewContext(context.Background(), model.NewId(), model.NewId(), model.NewId(), model.NewId(), model.NewId(), model.Session{}, nil) appErr = th.App.SendNotifyAdminPosts(ctx, "test", model.LicenseShortSkuProfessional, false) // try and send notification but workspace currentSKU has since changed to cloud-professional require.Nil(t, appErr) @@ -385,13 +384,13 @@ func Test_SendNotifyAdminPosts(t *testing.T) { t.Run("correctly send upgrade and install plugin post with the correct user request", func(t *testing.T) { th := Setup(t).InitBasic() defer th.TearDown() + ctx := request.EmptyContext(th.TestLogger) + os.Setenv("MM_NOTIFY_ADMIN_COOL_OFF_DAYS", "0") defer os.Unsetenv("MM_NOTIFY_ADMIN_COOL_OFF_DAYS") th.App.Srv().SetLicense(model.NewTestLicense("cloud")) - ctx := request.NewContext(context.Background(), model.NewId(), model.NewId(), model.NewId(), model.NewId(), model.NewId(), model.Session{}, nil) - // some notifications _, appErr := th.App.SaveAdminNotifyData(&model.NotifyAdminData{ UserId: th.BasicUser.Id, diff --git a/server/channels/app/oauth.go b/server/channels/app/oauth.go index 2a186b2fc72..1be59084079 100644 --- a/server/channels/app/oauth.go +++ b/server/channels/app/oauth.go @@ -21,8 +21,8 @@ import ( "github.com/mattermost/mattermost/server/public/model" "github.com/mattermost/mattermost/server/public/shared/i18n" "github.com/mattermost/mattermost/server/public/shared/mlog" + "github.com/mattermost/mattermost/server/public/shared/request" "github.com/mattermost/mattermost/server/v8/channels/app/platform" - "github.com/mattermost/mattermost/server/v8/channels/app/request" "github.com/mattermost/mattermost/server/v8/channels/store" "github.com/mattermost/mattermost/server/v8/channels/utils" "github.com/mattermost/mattermost/server/v8/einterfaces" @@ -431,7 +431,7 @@ func (a *App) newSessionUpdateToken(app *model.OAuthApp, accessData *model.Acces return accessRsp, nil } -func (a *App) GetOAuthLoginEndpoint(w http.ResponseWriter, r *http.Request, service, teamID, action, redirectTo, loginHint string, isMobile bool) (string, *model.AppError) { +func (a *App) GetOAuthLoginEndpoint(w http.ResponseWriter, r *http.Request, service, teamID, action, redirectTo, loginHint string, isMobile bool, desktopToken string) (string, *model.AppError) { stateProps := map[string]string{} stateProps["action"] = action if teamID != "" { @@ -442,6 +442,10 @@ func (a *App) GetOAuthLoginEndpoint(w http.ResponseWriter, r *http.Request, serv stateProps["redirect_to"] = redirectTo } + if desktopToken != "" { + stateProps["desktop_token"] = desktopToken + } + stateProps[model.UserAuthServiceIsMobile] = strconv.FormatBool(isMobile) authURL, err := a.GetAuthorizationCode(w, r, service, stateProps, loginHint) @@ -452,13 +456,17 @@ func (a *App) GetOAuthLoginEndpoint(w http.ResponseWriter, r *http.Request, serv return authURL, nil } -func (a *App) GetOAuthSignupEndpoint(w http.ResponseWriter, r *http.Request, service, teamID string) (string, *model.AppError) { +func (a *App) GetOAuthSignupEndpoint(w http.ResponseWriter, r *http.Request, service, teamID string, desktopToken string) (string, *model.AppError) { stateProps := map[string]string{} stateProps["action"] = model.OAuthActionSignup if teamID != "" { stateProps["team_id"] = teamID } + if desktopToken != "" { + stateProps["desktop_token"] = desktopToken + } + authURL, err := a.GetAuthorizationCode(w, r, service, stateProps, "") if err != nil { return "", err diff --git a/server/channels/app/onboarding.go b/server/channels/app/onboarding.go index 0184fd6f5cf..e085d803e40 100644 --- a/server/channels/app/onboarding.go +++ b/server/channels/app/onboarding.go @@ -10,7 +10,7 @@ import ( "github.com/mattermost/mattermost/server/public/model" "github.com/mattermost/mattermost/server/public/shared/mlog" - "github.com/mattermost/mattermost/server/v8/channels/app/request" + "github.com/mattermost/mattermost/server/public/shared/request" "github.com/mattermost/mattermost/server/v8/channels/store" ) diff --git a/server/channels/app/onboarding_test.go b/server/channels/app/onboarding_test.go index 9d7dd0e06f2..a9d5c60cd8c 100644 --- a/server/channels/app/onboarding_test.go +++ b/server/channels/app/onboarding_test.go @@ -9,7 +9,7 @@ import ( "github.com/stretchr/testify/require" mm_model "github.com/mattermost/mattermost/server/public/model" - "github.com/mattermost/mattermost/server/v8/channels/app/request" + "github.com/mattermost/mattermost/server/public/shared/request" ) func TestOnboardingSavesOrganizationName(t *testing.T) { diff --git a/server/channels/app/opentracing/opentracing_layer.go b/server/channels/app/opentracing/opentracing_layer.go index 70a2a07d752..94a91d7c8bf 100644 --- a/server/channels/app/opentracing/opentracing_layer.go +++ b/server/channels/app/opentracing/opentracing_layer.go @@ -22,10 +22,10 @@ import ( "github.com/mattermost/mattermost/server/public/plugin" "github.com/mattermost/mattermost/server/public/shared/i18n" "github.com/mattermost/mattermost/server/public/shared/mlog" + "github.com/mattermost/mattermost/server/public/shared/request" "github.com/mattermost/mattermost/server/public/shared/timezones" "github.com/mattermost/mattermost/server/v8/channels/app" "github.com/mattermost/mattermost/server/v8/channels/app/platform" - "github.com/mattermost/mattermost/server/v8/channels/app/request" "github.com/mattermost/mattermost/server/v8/channels/audit" "github.com/mattermost/mattermost/server/v8/channels/product" "github.com/mattermost/mattermost/server/v8/channels/store" @@ -37,6 +37,7 @@ import ( "github.com/mattermost/mattermost/server/v8/platform/services/tracing" "github.com/mattermost/mattermost/server/v8/platform/shared/filestore" "github.com/opentracing/opentracing-go/ext" + spanlog "github.com/opentracing/opentracing-go/log" ) @@ -1074,7 +1075,7 @@ func (a *OpenTracingAppLayer) CanNotifyAdmin(trial bool) bool { return resultVar0 } -func (a *OpenTracingAppLayer) CancelJob(jobId string) *model.AppError { +func (a *OpenTracingAppLayer) CancelJob(c *request.Context, jobId string) *model.AppError { origCtx := a.ctx span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.CancelJob") @@ -1086,7 +1087,7 @@ func (a *OpenTracingAppLayer) CancelJob(jobId string) *model.AppError { }() defer span.Finish() - resultVar0 := a.app.CancelJob(jobId) + resultVar0 := a.app.CancelJob(c, jobId) if resultVar0 != nil { span.LogFields(spanlog.Error(resultVar0)) @@ -1818,6 +1819,28 @@ func (a *OpenTracingAppLayer) ConvertBotToUser(c request.CTX, bot *model.Bot, us return resultVar0, resultVar1 } +func (a *OpenTracingAppLayer) ConvertGroupMessageToChannel(c request.CTX, convertedByUserId string, gmConversionRequest *model.GroupMessageConversionRequestBody) (*model.Channel, *model.AppError) { + origCtx := a.ctx + span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.ConvertGroupMessageToChannel") + + a.ctx = newCtx + a.app.Srv().Store().SetContext(newCtx) + defer func() { + a.app.Srv().Store().SetContext(origCtx) + a.ctx = origCtx + }() + + defer span.Finish() + resultVar0, resultVar1 := a.app.ConvertGroupMessageToChannel(c, convertedByUserId, gmConversionRequest) + + if resultVar1 != nil { + span.LogFields(spanlog.Error(resultVar1)) + ext.Error.Set(span, true) + } + + return resultVar0, resultVar1 +} + func (a *OpenTracingAppLayer) ConvertUserToBot(user *model.User) (*model.Bot, *model.AppError) { origCtx := a.ctx span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.ConvertUserToBot") @@ -2174,7 +2197,7 @@ func (a *OpenTracingAppLayer) CreateIncomingWebhookForChannel(creatorId string, return resultVar0, resultVar1 } -func (a *OpenTracingAppLayer) CreateJob(job *model.Job) (*model.Job, *model.AppError) { +func (a *OpenTracingAppLayer) CreateJob(c *request.Context, job *model.Job) (*model.Job, *model.AppError) { origCtx := a.ctx span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.CreateJob") @@ -2186,7 +2209,7 @@ func (a *OpenTracingAppLayer) CreateJob(job *model.Job) (*model.Job, *model.AppE }() defer span.Finish() - resultVar0, resultVar1 := a.app.CreateJob(job) + resultVar0, resultVar1 := a.app.CreateJob(c, job) if resultVar1 != nil { span.LogFields(spanlog.Error(resultVar1)) @@ -4539,6 +4562,28 @@ func (a *OpenTracingAppLayer) FinishSendAdminNotifyPost(trial bool, now int64, p a.app.FinishSendAdminNotifyPost(trial, now, pluginBasedData) } +func (a *OpenTracingAppLayer) GenerateAndSaveDesktopToken(createAt int64, user *model.User) (*string, *model.AppError) { + origCtx := a.ctx + span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GenerateAndSaveDesktopToken") + + a.ctx = newCtx + a.app.Srv().Store().SetContext(newCtx) + defer func() { + a.app.Srv().Store().SetContext(origCtx) + a.ctx = origCtx + }() + + defer span.Finish() + resultVar0, resultVar1 := a.app.GenerateAndSaveDesktopToken(createAt, user) + + if resultVar1 != nil { + span.LogFields(spanlog.Error(resultVar1)) + ext.Error.Set(span, true) + } + + return resultVar0, resultVar1 +} + func (a *OpenTracingAppLayer) GenerateMfaSecret(userID string) (*model.MfaSecret, *model.AppError) { origCtx := a.ctx span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GenerateMfaSecret") @@ -4600,7 +4645,7 @@ func (a *OpenTracingAppLayer) GeneratePublicLink(siteURL string, info *model.Fil return resultVar0 } -func (a *OpenTracingAppLayer) GenerateSupportPacket() []model.FileData { +func (a *OpenTracingAppLayer) GenerateSupportPacket(c *request.Context) []model.FileData { origCtx := a.ctx span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GenerateSupportPacket") @@ -4612,7 +4657,7 @@ func (a *OpenTracingAppLayer) GenerateSupportPacket() []model.FileData { }() defer span.Finish() - resultVar0 := a.app.GenerateSupportPacket() + resultVar0 := a.app.GenerateSupportPacket(c) return resultVar0 } @@ -6643,6 +6688,28 @@ func (a *OpenTracingAppLayer) GetGroupMemberUsersSortedPage(groupID string, page return resultVar0, resultVar1, resultVar2 } +func (a *OpenTracingAppLayer) GetGroupMessageMembersCommonTeams(c request.CTX, channelID string) ([]*model.Team, *model.AppError) { + origCtx := a.ctx + span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetGroupMessageMembersCommonTeams") + + a.ctx = newCtx + a.app.Srv().Store().SetContext(newCtx) + defer func() { + a.app.Srv().Store().SetContext(origCtx) + a.ctx = origCtx + }() + + defer span.Finish() + resultVar0, resultVar1 := a.app.GetGroupMessageMembersCommonTeams(c, channelID) + + if resultVar1 != nil { + span.LogFields(spanlog.Error(resultVar1)) + ext.Error.Set(span, true) + } + + return resultVar0, resultVar1 +} + func (a *OpenTracingAppLayer) GetGroupSyncable(groupID string, syncableID string, syncableType model.GroupSyncableType) (*model.GroupSyncable, *model.AppError) { origCtx := a.ctx span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetGroupSyncable") @@ -6968,7 +7035,7 @@ func (a *OpenTracingAppLayer) GetIncomingWebhooksPageByUser(userID string, page return resultVar0, resultVar1 } -func (a *OpenTracingAppLayer) GetJob(id string) (*model.Job, *model.AppError) { +func (a *OpenTracingAppLayer) GetJob(c *request.Context, id string) (*model.Job, *model.AppError) { origCtx := a.ctx span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetJob") @@ -6980,7 +7047,7 @@ func (a *OpenTracingAppLayer) GetJob(id string) (*model.Job, *model.AppError) { }() defer span.Finish() - resultVar0, resultVar1 := a.app.GetJob(id) + resultVar0, resultVar1 := a.app.GetJob(c, id) if resultVar1 != nil { span.LogFields(spanlog.Error(resultVar1)) @@ -6990,29 +7057,7 @@ func (a *OpenTracingAppLayer) GetJob(id string) (*model.Job, *model.AppError) { return resultVar0, resultVar1 } -func (a *OpenTracingAppLayer) GetJobs(offset int, limit int) ([]*model.Job, *model.AppError) { - origCtx := a.ctx - span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetJobs") - - a.ctx = newCtx - a.app.Srv().Store().SetContext(newCtx) - defer func() { - a.app.Srv().Store().SetContext(origCtx) - a.ctx = origCtx - }() - - defer span.Finish() - resultVar0, resultVar1 := a.app.GetJobs(offset, limit) - - if resultVar1 != nil { - span.LogFields(spanlog.Error(resultVar1)) - ext.Error.Set(span, true) - } - - return resultVar0, resultVar1 -} - -func (a *OpenTracingAppLayer) GetJobsByType(jobType string, offset int, limit int) ([]*model.Job, *model.AppError) { +func (a *OpenTracingAppLayer) GetJobsByType(c *request.Context, jobType string, offset int, limit int) ([]*model.Job, *model.AppError) { origCtx := a.ctx span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetJobsByType") @@ -7024,7 +7069,7 @@ func (a *OpenTracingAppLayer) GetJobsByType(jobType string, offset int, limit in }() defer span.Finish() - resultVar0, resultVar1 := a.app.GetJobsByType(jobType, offset, limit) + resultVar0, resultVar1 := a.app.GetJobsByType(c, jobType, offset, limit) if resultVar1 != nil { span.LogFields(spanlog.Error(resultVar1)) @@ -7034,7 +7079,7 @@ func (a *OpenTracingAppLayer) GetJobsByType(jobType string, offset int, limit in return resultVar0, resultVar1 } -func (a *OpenTracingAppLayer) GetJobsByTypePage(jobType string, page int, perPage int) ([]*model.Job, *model.AppError) { +func (a *OpenTracingAppLayer) GetJobsByTypePage(c *request.Context, jobType string, page int, perPage int) ([]*model.Job, *model.AppError) { origCtx := a.ctx span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetJobsByTypePage") @@ -7046,7 +7091,7 @@ func (a *OpenTracingAppLayer) GetJobsByTypePage(jobType string, page int, perPag }() defer span.Finish() - resultVar0, resultVar1 := a.app.GetJobsByTypePage(jobType, page, perPage) + resultVar0, resultVar1 := a.app.GetJobsByTypePage(c, jobType, page, perPage) if resultVar1 != nil { span.LogFields(spanlog.Error(resultVar1)) @@ -7056,7 +7101,7 @@ func (a *OpenTracingAppLayer) GetJobsByTypePage(jobType string, page int, perPag return resultVar0, resultVar1 } -func (a *OpenTracingAppLayer) GetJobsByTypes(jobTypes []string, offset int, limit int) ([]*model.Job, *model.AppError) { +func (a *OpenTracingAppLayer) GetJobsByTypes(c *request.Context, jobTypes []string, offset int, limit int) ([]*model.Job, *model.AppError) { origCtx := a.ctx span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetJobsByTypes") @@ -7068,7 +7113,7 @@ func (a *OpenTracingAppLayer) GetJobsByTypes(jobTypes []string, offset int, limi }() defer span.Finish() - resultVar0, resultVar1 := a.app.GetJobsByTypes(jobTypes, offset, limit) + resultVar0, resultVar1 := a.app.GetJobsByTypes(c, jobTypes, offset, limit) if resultVar1 != nil { span.LogFields(spanlog.Error(resultVar1)) @@ -7078,7 +7123,7 @@ func (a *OpenTracingAppLayer) GetJobsByTypes(jobTypes []string, offset int, limi return resultVar0, resultVar1 } -func (a *OpenTracingAppLayer) GetJobsByTypesPage(jobType []string, page int, perPage int) ([]*model.Job, *model.AppError) { +func (a *OpenTracingAppLayer) GetJobsByTypesPage(c *request.Context, jobType []string, page int, perPage int) ([]*model.Job, *model.AppError) { origCtx := a.ctx span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetJobsByTypesPage") @@ -7090,29 +7135,7 @@ func (a *OpenTracingAppLayer) GetJobsByTypesPage(jobType []string, page int, per }() defer span.Finish() - resultVar0, resultVar1 := a.app.GetJobsByTypesPage(jobType, page, perPage) - - if resultVar1 != nil { - span.LogFields(spanlog.Error(resultVar1)) - ext.Error.Set(span, true) - } - - return resultVar0, resultVar1 -} - -func (a *OpenTracingAppLayer) GetJobsPage(page int, perPage int) ([]*model.Job, *model.AppError) { - origCtx := a.ctx - span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetJobsPage") - - a.ctx = newCtx - a.app.Srv().Store().SetContext(newCtx) - defer func() { - a.app.Srv().Store().SetContext(origCtx) - a.ctx = origCtx - }() - - defer span.Finish() - resultVar0, resultVar1 := a.app.GetJobsPage(page, perPage) + resultVar0, resultVar1 := a.app.GetJobsByTypesPage(c, jobType, page, perPage) if resultVar1 != nil { span.LogFields(spanlog.Error(resultVar1)) @@ -7613,7 +7636,7 @@ func (a *OpenTracingAppLayer) GetOAuthImplicitRedirect(userID string, authReques return resultVar0, resultVar1 } -func (a *OpenTracingAppLayer) GetOAuthLoginEndpoint(w http.ResponseWriter, r *http.Request, service string, teamID string, action string, redirectTo string, loginHint string, isMobile bool) (string, *model.AppError) { +func (a *OpenTracingAppLayer) GetOAuthLoginEndpoint(w http.ResponseWriter, r *http.Request, service string, teamID string, action string, redirectTo string, loginHint string, isMobile bool, desktopToken string) (string, *model.AppError) { origCtx := a.ctx span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetOAuthLoginEndpoint") @@ -7625,7 +7648,7 @@ func (a *OpenTracingAppLayer) GetOAuthLoginEndpoint(w http.ResponseWriter, r *ht }() defer span.Finish() - resultVar0, resultVar1 := a.app.GetOAuthLoginEndpoint(w, r, service, teamID, action, redirectTo, loginHint, isMobile) + resultVar0, resultVar1 := a.app.GetOAuthLoginEndpoint(w, r, service, teamID, action, redirectTo, loginHint, isMobile, desktopToken) if resultVar1 != nil { span.LogFields(spanlog.Error(resultVar1)) @@ -7635,7 +7658,7 @@ func (a *OpenTracingAppLayer) GetOAuthLoginEndpoint(w http.ResponseWriter, r *ht return resultVar0, resultVar1 } -func (a *OpenTracingAppLayer) GetOAuthSignupEndpoint(w http.ResponseWriter, r *http.Request, service string, teamID string) (string, *model.AppError) { +func (a *OpenTracingAppLayer) GetOAuthSignupEndpoint(w http.ResponseWriter, r *http.Request, service string, teamID string, desktopToken string) (string, *model.AppError) { origCtx := a.ctx span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetOAuthSignupEndpoint") @@ -7647,7 +7670,7 @@ func (a *OpenTracingAppLayer) GetOAuthSignupEndpoint(w http.ResponseWriter, r *h }() defer span.Finish() - resultVar0, resultVar1 := a.app.GetOAuthSignupEndpoint(w, r, service, teamID) + resultVar0, resultVar1 := a.app.GetOAuthSignupEndpoint(w, r, service, teamID, desktopToken) if resultVar1 != nil { span.LogFields(spanlog.Error(resultVar1)) @@ -10489,6 +10512,28 @@ func (a *OpenTracingAppLayer) GetUserByEmail(email string) (*model.User, *model. return resultVar0, resultVar1 } +func (a *OpenTracingAppLayer) GetUserByRemoteID(remoteID string) (*model.User, *model.AppError) { + origCtx := a.ctx + span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetUserByRemoteID") + + a.ctx = newCtx + a.app.Srv().Store().SetContext(newCtx) + defer func() { + a.app.Srv().Store().SetContext(origCtx) + a.ctx = origCtx + }() + + defer span.Finish() + resultVar0, resultVar1 := a.app.GetUserByRemoteID(remoteID) + + if resultVar1 != nil { + span.LogFields(spanlog.Error(resultVar1)) + ext.Error.Set(span, true) + } + + return resultVar0, resultVar1 +} + func (a *OpenTracingAppLayer) GetUserByUsername(username string) (*model.User, *model.AppError) { origCtx := a.ctx span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetUserByUsername") @@ -11663,7 +11708,7 @@ func (a *OpenTracingAppLayer) InstallPlugin(pluginFile io.ReadSeeker, replace bo return resultVar0, resultVar1 } -func (a *OpenTracingAppLayer) InvalidateAllEmailInvites() *model.AppError { +func (a *OpenTracingAppLayer) InvalidateAllEmailInvites(c *request.Context) *model.AppError { origCtx := a.ctx span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.InvalidateAllEmailInvites") @@ -11675,7 +11720,7 @@ func (a *OpenTracingAppLayer) InvalidateAllEmailInvites() *model.AppError { }() defer span.Finish() - resultVar0 := a.app.InvalidateAllEmailInvites() + resultVar0 := a.app.InvalidateAllEmailInvites(c) if resultVar0 != nil { span.LogFields(spanlog.Error(resultVar0)) @@ -11685,7 +11730,7 @@ func (a *OpenTracingAppLayer) InvalidateAllEmailInvites() *model.AppError { return resultVar0 } -func (a *OpenTracingAppLayer) InvalidateAllResendInviteEmailJobs() *model.AppError { +func (a *OpenTracingAppLayer) InvalidateAllResendInviteEmailJobs(c *request.Context) *model.AppError { origCtx := a.ctx span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.InvalidateAllResendInviteEmailJobs") @@ -11697,7 +11742,7 @@ func (a *OpenTracingAppLayer) InvalidateAllResendInviteEmailJobs() *model.AppErr }() defer span.Finish() - resultVar0 := a.app.InvalidateAllResendInviteEmailJobs() + resultVar0 := a.app.InvalidateAllResendInviteEmailJobs(c) if resultVar0 != nil { span.LogFields(spanlog.Error(resultVar0)) @@ -16094,6 +16139,28 @@ func (a *OpenTracingAppLayer) SetDefaultProfileImage(c request.CTX, user *model. return resultVar0 } +func (a *OpenTracingAppLayer) SetFileSearchableContent(fileID string, data string) *model.AppError { + origCtx := a.ctx + span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SetFileSearchableContent") + + a.ctx = newCtx + a.app.Srv().Store().SetContext(newCtx) + defer func() { + a.app.Srv().Store().SetContext(origCtx) + a.ctx = origCtx + }() + + defer span.Finish() + resultVar0 := a.app.SetFileSearchableContent(fileID, data) + + if resultVar0 != nil { + span.LogFields(spanlog.Error(resultVar0)) + ext.Error.Set(span, true) + } + + return resultVar0 +} + func (a *OpenTracingAppLayer) SetPhase2PermissionsMigrationStatus(isComplete bool) error { origCtx := a.ctx span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SetPhase2PermissionsMigrationStatus") @@ -16669,7 +16736,7 @@ func (a *OpenTracingAppLayer) SwitchOAuthToEmail(email string, password string, return resultVar0, resultVar1 } -func (a *OpenTracingAppLayer) SyncLdap(includeRemovedMembers bool) { +func (a *OpenTracingAppLayer) SyncLdap(c *request.Context, includeRemovedMembers bool) { origCtx := a.ctx span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SyncLdap") @@ -16681,7 +16748,7 @@ func (a *OpenTracingAppLayer) SyncLdap(includeRemovedMembers bool) { }() defer span.Finish() - a.app.SyncLdap(includeRemovedMembers) + a.app.SyncLdap(c, includeRemovedMembers) } func (a *OpenTracingAppLayer) SyncPlugins() *model.AppError { @@ -17639,7 +17706,7 @@ func (a *OpenTracingAppLayer) UpdatePasswordSendEmail(c request.CTX, user *model return resultVar0 } -func (a *OpenTracingAppLayer) UpdatePost(c *request.Context, post *model.Post, safeUpdate bool) (*model.Post, *model.AppError) { +func (a *OpenTracingAppLayer) UpdatePost(c *request.Context, receivedUpdatedPost *model.Post, safeUpdate bool) (*model.Post, *model.AppError) { origCtx := a.ctx span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.UpdatePost") @@ -17651,7 +17718,7 @@ func (a *OpenTracingAppLayer) UpdatePost(c *request.Context, post *model.Post, s }() defer span.Finish() - resultVar0, resultVar1 := a.app.UpdatePost(c, post, safeUpdate) + resultVar0, resultVar1 := a.app.UpdatePost(c, receivedUpdatedPost, safeUpdate) if resultVar1 != nil { span.LogFields(spanlog.Error(resultVar1)) @@ -18351,6 +18418,28 @@ func (a *OpenTracingAppLayer) UploadFile(c request.CTX, data []byte, channelID s return resultVar0, resultVar1 } +func (a *OpenTracingAppLayer) UploadFileForUserAndTeam(c request.CTX, data []byte, channelID string, filename string, rawUserId string, rawTeamId string) (*model.FileInfo, *model.AppError) { + origCtx := a.ctx + span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.UploadFileForUserAndTeam") + + a.ctx = newCtx + a.app.Srv().Store().SetContext(newCtx) + defer func() { + a.app.Srv().Store().SetContext(origCtx) + a.ctx = origCtx + }() + + defer span.Finish() + resultVar0, resultVar1 := a.app.UploadFileForUserAndTeam(c, data, channelID, filename, rawUserId, rawTeamId) + + if resultVar1 != nil { + span.LogFields(spanlog.Error(resultVar1)) + ext.Error.Set(span, true) + } + + return resultVar0, resultVar1 +} + func (a *OpenTracingAppLayer) UploadFileX(c *request.Context, channelID string, name string, input io.Reader, opts ...func(*app.UploadFileTask)) (*model.FileInfo, *model.AppError) { origCtx := a.ctx span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.UploadFileX") @@ -18539,6 +18628,28 @@ func (a *OpenTracingAppLayer) UserIsInAdminRoleGroup(userID string, syncableID s return resultVar0, resultVar1 } +func (a *OpenTracingAppLayer) ValidateDesktopToken(token string, expiryTime int64) (*model.User, *model.AppError) { + origCtx := a.ctx + span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.ValidateDesktopToken") + + a.ctx = newCtx + a.app.Srv().Store().SetContext(newCtx) + defer func() { + a.app.Srv().Store().SetContext(origCtx) + a.ctx = origCtx + }() + + defer span.Finish() + resultVar0, resultVar1 := a.app.ValidateDesktopToken(token, expiryTime) + + if resultVar1 != nil { + span.LogFields(spanlog.Error(resultVar1)) + ext.Error.Set(span, true) + } + + return resultVar0, resultVar1 +} + func (a *OpenTracingAppLayer) ValidateUserPermissionsOnChannels(c request.CTX, userId string, channelIds []string) []string { origCtx := a.ctx span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.ValidateUserPermissionsOnChannels") diff --git a/server/channels/app/permissions.go b/server/channels/app/permissions.go index 31fc9bd5763..26d6a614861 100644 --- a/server/channels/app/permissions.go +++ b/server/channels/app/permissions.go @@ -14,7 +14,7 @@ import ( "github.com/pkg/errors" "github.com/mattermost/mattermost/server/public/model" - "github.com/mattermost/mattermost/server/v8/channels/app/request" + "github.com/mattermost/mattermost/server/public/shared/request" "github.com/mattermost/mattermost/server/v8/channels/product" ) diff --git a/server/channels/app/platform/cluster_handlers.go b/server/channels/app/platform/cluster_handlers.go index f92aec3a0cd..a45809085b6 100644 --- a/server/channels/app/platform/cluster_handlers.go +++ b/server/channels/app/platform/cluster_handlers.go @@ -7,7 +7,6 @@ import ( "bytes" "encoding/json" "fmt" - "runtime/debug" "github.com/mattermost/mattermost/server/public/model" "github.com/mattermost/mattermost/server/public/shared/mlog" @@ -159,7 +158,6 @@ func (ps *PlatformService) InvalidateAllCachesSkipSend() { } func (ps *PlatformService) InvalidateAllCaches() *model.AppError { - debug.FreeOSMemory() ps.InvalidateAllCachesSkipSend() if ps.clusterIFace != nil { diff --git a/server/channels/app/platform/license.go b/server/channels/app/platform/license.go index d3a1b0ecb5c..d71348d9e96 100644 --- a/server/channels/app/platform/license.go +++ b/server/channels/app/platform/license.go @@ -5,6 +5,7 @@ package platform import ( "bytes" + "context" "encoding/json" "fmt" "net/http" @@ -17,6 +18,7 @@ import ( "github.com/mattermost/mattermost/server/public/model" "github.com/mattermost/mattermost/server/public/shared/mlog" "github.com/mattermost/mattermost/server/v8/channels/jobs" + "github.com/mattermost/mattermost/server/v8/channels/store/sqlstore" "github.com/mattermost/mattermost/server/v8/channels/utils" "github.com/mattermost/mattermost/server/v8/einterfaces" ) @@ -95,7 +97,7 @@ func (ps *PlatformService) LoadLicense() { } } - record, nErr := ps.Store.License().Get(licenseId) + record, nErr := ps.Store.License().Get(sqlstore.WithMaster(context.Background()), licenseId) if nErr != nil { ps.logger.Error("License key from https://mattermost.com required to unlock enterprise features.", mlog.Err(nErr)) ps.SetLicense(nil) @@ -166,7 +168,7 @@ func (ps *PlatformService) SaveLicense(licenseBytes []byte) (*model.License, *mo record.Id = license.Id record.Bytes = string(licenseBytes) - _, nErr := ps.Store.License().Save(record) + nErr := ps.Store.License().Save(record) if nErr != nil { ps.RemoveLicense() var appErr *model.AppError diff --git a/server/channels/app/platform/log.go b/server/channels/app/platform/log.go index 5b1b7d28d6e..8021267d7bd 100644 --- a/server/channels/app/platform/log.go +++ b/server/channels/app/platform/log.go @@ -13,6 +13,7 @@ import ( "os" "time" + "github.com/mattermost/logr/v2" "github.com/mattermost/mattermost/server/public/model" "github.com/mattermost/mattermost/server/public/shared/mlog" "github.com/mattermost/mattermost/server/v8/config" @@ -31,7 +32,7 @@ func (ps *PlatformService) initLogging() error { // create the app logger if needed if ps.logger == nil { var err error - ps.logger, err = mlog.NewLogger() + ps.logger, err = mlog.NewLogger(logr.MaxFieldLen(*ps.Config().LogSettings.MaxFieldSize)) if err != nil { return err } diff --git a/server/channels/app/platform/mocks/SuiteIFace.go b/server/channels/app/platform/mocks/SuiteIFace.go index a5f377280fd..7a147902c2d 100644 --- a/server/channels/app/platform/mocks/SuiteIFace.go +++ b/server/channels/app/platform/mocks/SuiteIFace.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.14.0. DO NOT EDIT. +// Code generated by mockery v2.32.2. DO NOT EDIT. // Regenerate this file using `make platform-mocks`. @@ -19,6 +19,10 @@ func (_m *SuiteIFace) GetSession(token string) (*model.Session, *model.AppError) ret := _m.Called(token) var r0 *model.Session + var r1 *model.AppError + if rf, ok := ret.Get(0).(func(string) (*model.Session, *model.AppError)); ok { + return rf(token) + } if rf, ok := ret.Get(0).(func(string) *model.Session); ok { r0 = rf(token) } else { @@ -27,7 +31,6 @@ func (_m *SuiteIFace) GetSession(token string) (*model.Session, *model.AppError) } } - var r1 *model.AppError if rf, ok := ret.Get(1).(func(string) *model.AppError); ok { r1 = rf(token) } else { @@ -58,13 +61,16 @@ func (_m *SuiteIFace) UserCanSeeOtherUser(userID string, otherUserId string) (bo ret := _m.Called(userID, otherUserId) var r0 bool + var r1 *model.AppError + if rf, ok := ret.Get(0).(func(string, string) (bool, *model.AppError)); ok { + return rf(userID, otherUserId) + } if rf, ok := ret.Get(0).(func(string, string) bool); ok { r0 = rf(userID, otherUserId) } else { r0 = ret.Get(0).(bool) } - var r1 *model.AppError if rf, ok := ret.Get(1).(func(string, string) *model.AppError); ok { r1 = rf(userID, otherUserId) } else { @@ -76,13 +82,12 @@ func (_m *SuiteIFace) UserCanSeeOtherUser(userID string, otherUserId string) (bo return r0, r1 } -type mockConstructorTestingTNewSuiteIFace interface { +// NewSuiteIFace creates a new instance of SuiteIFace. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewSuiteIFace(t interface { mock.TestingT Cleanup(func()) -} - -// NewSuiteIFace creates a new instance of SuiteIFace. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -func NewSuiteIFace(t mockConstructorTestingTNewSuiteIFace) *SuiteIFace { +}) *SuiteIFace { mock := &SuiteIFace{} mock.Mock.Test(t) diff --git a/server/channels/app/platform/web_conn.go b/server/channels/app/platform/web_conn.go index 5994f500a1e..3e04d1c3420 100644 --- a/server/channels/app/platform/web_conn.go +++ b/server/channels/app/platform/web_conn.go @@ -98,9 +98,7 @@ type WebConn struct { deadQueuePointer int // active indicates whether there is an open websocket connection attached // to this webConn or not. - // It is not used as an atomic, because there is no need to. - // So do not use this outside the web hub. - active bool + active atomic.Bool // reuseCount indicates how many times this connection has been reused. // This is used to differentiate between a fresh connection and // a reused connection. @@ -220,7 +218,6 @@ func (ps *PlatformService) NewWebConn(cfg *WebConnConfig, suite SuiteIFace, runn UserId: cfg.Session.UserId, T: cfg.TFunc, Locale: cfg.Locale, - active: cfg.Active, reuseCount: cfg.ReuseCount, endWritePump: make(chan struct{}), pumpFinished: make(chan struct{}), @@ -228,6 +225,7 @@ func (ps *PlatformService) NewWebConn(cfg *WebConnConfig, suite SuiteIFace, runn lastLogTimeSlow: time.Now(), lastLogTimeFull: time.Now(), } + wc.active.Store(cfg.Active) wc.SetSession(&cfg.Session) wc.SetSessionToken(cfg.Session.Token) @@ -295,7 +293,7 @@ func (wc *WebConn) GetConnectionID() string { // are inactive or not. func areAllInactive(conns []*WebConn) bool { for _, conn := range conns { - if conn.active { + if conn.active.Load() { return false } } @@ -474,7 +472,7 @@ func (wc *WebConn) writePump() { continue } - if len(wc.send) >= sendFullWarn && time.Since(wc.lastLogTimeFull) > websocketSuppressWarnThreshold { + if wc.active.Load() && len(wc.send) >= sendFullWarn && time.Since(wc.lastLogTimeFull) > websocketSuppressWarnThreshold { logData := []mlog.Field{ mlog.String("user_id", wc.UserId), mlog.String("conn_id", wc.GetConnectionID()), @@ -726,8 +724,8 @@ func (wc *WebConn) ShouldSendEvent(msg *model.WebSocketEvent) bool { switch msg.EventType() { case model.WebsocketEventTyping, model.WebsocketEventStatusChange, - model.WebsocketEventChannelViewed: - if time.Since(wc.lastLogTimeSlow) > websocketSuppressWarnThreshold { + model.WebsocketEventMultipleChannelsViewed: + if wc.active.Load() && time.Since(wc.lastLogTimeSlow) > websocketSuppressWarnThreshold { mlog.Warn( "websocket.slow: dropping message", mlog.String("user_id", wc.UserId), diff --git a/server/channels/app/platform/web_hub.go b/server/channels/app/platform/web_hub.go index 5717b042c0c..8630cb5bb43 100644 --- a/server/channels/app/platform/web_hub.go +++ b/server/channels/app/platform/web_hub.go @@ -383,7 +383,7 @@ func (h *Hub) Start() { conns := connIndex.ForUser(webSessionMessage.userID) var isRegistered bool for _, conn := range conns { - if !conn.active { + if !conn.active.Load() { continue } if conn.GetSessionToken() == webSessionMessage.sessionToken { @@ -411,7 +411,7 @@ func (h *Hub) Start() { // Mark the current one as active. // There is no need to check if it was inactive or not, // we will anyways need to make it active. - webConn.active = true + webConn.active.Store(true) connIndex.Add(webConn) atomic.StoreInt64(&h.connectionCount, int64(connIndex.AllActive())) @@ -426,7 +426,7 @@ func (h *Hub) Start() { case webConn := <-h.unregister: // If already removed (via queue full), then removing again becomes a noop. // But if not removed, mark inactive. - webConn.active = false + webConn.active.Store(false) atomic.StoreInt64(&h.connectionCount, int64(connIndex.AllActive())) @@ -444,7 +444,7 @@ func (h *Hub) Start() { } var latestActivity int64 for _, conn := range conns { - if !conn.active { + if !conn.active.Load() { continue } if conn.lastUserActivityAt > latestActivity { @@ -464,7 +464,7 @@ func (h *Hub) Start() { } case activity := <-h.activity: for _, webConn := range connIndex.ForUser(activity.userID) { - if !webConn.active { + if !webConn.active.Load() { continue } if webConn.GetSessionToken() == activity.sessionToken { @@ -478,9 +478,12 @@ func (h *Hub) Start() { select { case directMsg.conn.send <- directMsg.msg: default: - mlog.Error("webhub.broadcast: cannot send, closing websocket for user", - mlog.String("user_id", directMsg.conn.UserId), - mlog.String("conn_id", directMsg.conn.GetConnectionID())) + // Don't log the warning if it's an inactive connection. + if directMsg.conn.active.Load() { + mlog.Error("webhub.broadcast: cannot send, closing websocket for user", + mlog.String("user_id", directMsg.conn.UserId), + mlog.String("conn_id", directMsg.conn.GetConnectionID())) + } close(directMsg.conn.send) connIndex.Remove(directMsg.conn) } @@ -497,9 +500,12 @@ func (h *Hub) Start() { select { case webConn.send <- msg: default: - mlog.Error("webhub.broadcast: cannot send, closing websocket for user", - mlog.String("user_id", webConn.UserId), - mlog.String("conn_id", webConn.GetConnectionID())) + // Don't log the warning if it's an inactive connection. + if webConn.active.Load() { + mlog.Error("webhub.broadcast: cannot send, closing websocket for user", + mlog.String("user_id", webConn.UserId), + mlog.String("conn_id", webConn.GetConnectionID())) + } close(webConn.send) connIndex.Remove(webConn) } @@ -639,7 +645,7 @@ func (i *hubConnectionIndex) RemoveInactiveByConnectionID(userID, connectionID s return nil } for _, conn := range i.ForUser(userID) { - if conn.GetConnectionID() == connectionID && !conn.active { + if conn.GetConnectionID() == connectionID && !conn.active.Load() { i.Remove(conn) return conn } @@ -652,7 +658,7 @@ func (i *hubConnectionIndex) RemoveInactiveByConnectionID(userID, connectionID s func (i *hubConnectionIndex) RemoveInactiveConnections() { now := model.GetMillis() for conn := range i.byConnection { - if !conn.active && now-conn.lastUserActivityAt > i.staleThreshold.Milliseconds() { + if !conn.active.Load() && now-conn.lastUserActivityAt > i.staleThreshold.Milliseconds() { i.Remove(conn) } } @@ -664,7 +670,7 @@ func (i *hubConnectionIndex) RemoveInactiveConnections() { func (i *hubConnectionIndex) AllActive() int { cnt := 0 for conn := range i.byConnection { - if conn.active { + if conn.active.Load() { cnt++ } } diff --git a/server/channels/app/platform/web_hub_test.go b/server/channels/app/platform/web_hub_test.go index 7277380d2e3..79128ae89ce 100644 --- a/server/channels/app/platform/web_hub_test.go +++ b/server/channels/app/platform/web_hub_test.go @@ -379,8 +379,8 @@ func TestHubConnIndexInactive(t *testing.T) { wc1 := &WebConn{ Platform: th.Service, UserId: model.NewId(), - active: true, } + wc1.active.Store(true) wc1.SetConnectionID("conn1") wc1.SetSession(&model.Session{}) @@ -388,16 +388,16 @@ func TestHubConnIndexInactive(t *testing.T) { wc2 := &WebConn{ Platform: th.Service, UserId: model.NewId(), - active: true, } + wc2.active.Store(true) wc2.SetConnectionID("conn2") wc2.SetSession(&model.Session{}) wc3 := &WebConn{ Platform: th.Service, UserId: wc2.UserId, - active: false, } + wc3.active.Store(false) wc3.SetConnectionID("conn3") wc3.SetSession(&model.Session{}) diff --git a/server/channels/app/plugin.go b/server/channels/app/plugin.go index 51da55dc438..1b7039bdb18 100644 --- a/server/channels/app/plugin.go +++ b/server/channels/app/plugin.go @@ -4,6 +4,7 @@ package app import ( + "bytes" "encoding/base64" "fmt" "io" @@ -15,7 +16,7 @@ import ( "strings" "sync" - "github.com/blang/semver" + "github.com/blang/semver/v4" "github.com/gorilla/mux" svg "github.com/h2non/go-is-svg" "github.com/pkg/errors" @@ -23,11 +24,10 @@ import ( "github.com/mattermost/mattermost/server/public/model" "github.com/mattermost/mattermost/server/public/plugin" "github.com/mattermost/mattermost/server/public/shared/mlog" - "github.com/mattermost/mattermost/server/v8/channels/app/request" + "github.com/mattermost/mattermost/server/public/shared/request" "github.com/mattermost/mattermost/server/v8/channels/product" "github.com/mattermost/mattermost/server/v8/channels/utils/fileutils" "github.com/mattermost/mattermost/server/v8/platform/services/marketplace" - "github.com/mattermost/mattermost/server/v8/platform/shared/filestore" ) // prepackagedPluginsDir is the hard-coded folder name where prepackaged plugins are bundled @@ -253,13 +253,13 @@ func (ch *Channels) initPlugins(c *request.Context, pluginDir, webappPluginDir s ch.srv.Log().Error("Failed to sync plugins from the file store", mlog.Err(err)) } - plugins := ch.processPrepackagedPlugins(prepackagedPluginsDir) - pluginsEnvironment = ch.GetPluginsEnvironment() - if pluginsEnvironment == nil { - ch.srv.Log().Info("Plugins environment not found, server is likely shutting down") - return + if err := ch.processPrepackagedPlugins(prepackagedPluginsDir); err != nil { + ch.srv.Log().Error("Failed to process prepackaged plugins", mlog.Err(err)) } - pluginsEnvironment.SetPrepackagedPlugins(plugins) + ch.pluginClusterLeaderListenerID = ch.srv.AddClusterLeaderChangedListener(func() { + ch.persistTransitionallyPrepackagedPlugins() + }) + ch.persistTransitionallyPrepackagedPlugins() // Sync plugin active state when config changes. Also notify plugins. ch.pluginsLock.Lock() @@ -352,18 +352,22 @@ func (ch *Channels) syncPlugins() *model.AppError { } defer bundle.Close() - var signature filestore.ReadCloseSeeker if *ch.cfgSvc.Config().PluginSettings.RequirePluginSignature { - signature, appErr = ch.srv.fileReader(plugin.signaturePath) + signature, appErr := ch.srv.fileReader(plugin.signaturePath) if appErr != nil { logger.Error("Failed to open plugin signature from file store.", mlog.Err(appErr)) return } defer signature.Close() + + if appErr = ch.verifyPlugin(bundle, signature); appErr != nil { + logger.Error("Failed to validate plugin signature", mlog.Err(appErr)) + return + } } logger.Info("Syncing plugin from file store") - if _, err := ch.installPluginLocally(bundle, signature, installPluginLocallyAlways); err != nil && err.Id != "app.plugin.skip_installation.app_error" { + if _, err := ch.installPluginLocally(bundle, installPluginLocallyAlways); err != nil && err.Id != "app.plugin.skip_installation.app_error" { logger.Error("Failed to sync plugin from file store", mlog.Err(err)) } }(plugin) @@ -388,6 +392,8 @@ func (ch *Channels) ShutDownPlugins() { ch.RemoveConfigListener(ch.pluginConfigListenerID) ch.pluginConfigListenerID = "" + ch.srv.RemoveClusterLeaderChangedListener(ch.pluginClusterLeaderListenerID) + ch.pluginClusterLeaderListenerID = "" // Acquiring lock manually before cleaning up PluginsEnvironment. ch.pluginsLock.Lock() @@ -555,20 +561,12 @@ func (a *App) GetMarketplacePlugins(filter *model.MarketplacePluginFilter) ([]*m } if !filter.RemoteOnly { - // Some plugin don't work on cloud. The remote Marketplace is aware of this fact, - // but prepackaged plugins are not. Hence, on a cloud installation prepackaged plugins - // shouldn't be shown in the Marketplace modal. - // This is a short term fix. The long term solution is to have a separate set of - // prepacked plugins for cloud: https://mattermost.atlassian.net/browse/MM-31331. - license := a.Srv().License() - if license == nil || !license.IsCloud() { - appErr := a.mergePrepackagedPlugins(plugins) - if appErr != nil { - return nil, appErr - } + appErr := a.mergePrepackagedPlugins(plugins) + if appErr != nil { + return nil, appErr } - appErr := a.mergeLocalPlugins(plugins) + appErr = a.mergeLocalPlugins(plugins) if appErr != nil { return nil, appErr } @@ -911,26 +909,48 @@ func (ch *Channels) getPluginsFromFilePaths(fileStorePaths []string) map[string] return pluginSignaturePathMap } -func (ch *Channels) processPrepackagedPlugins(pluginsDir string) []*plugin.PrepackagedPlugin { - prepackagedPluginsDir, found := fileutils.FindDir(pluginsDir) +// processPrepackagedPlugins processes the plugins prepackaged with this server in the +// prepackaged_plugins directory. +// +// If enabled, prepackaged plugins are installed or upgraded locally. A list of transitionally +// prepackaged plugins is also collected for later persistence to the filestore. +func (ch *Channels) processPrepackagedPlugins(prepackagedPluginsDir string) error { + prepackagedPluginsPath, found := fileutils.FindDir(prepackagedPluginsDir) if !found { ch.srv.Log().Debug("No prepackaged plugins directory found") return nil } + ch.srv.Log().Debug("Processing prepackaged plugins in directory", mlog.String("path", prepackagedPluginsPath)) + var fileStorePaths []string - err := filepath.Walk(prepackagedPluginsDir, func(walkPath string, info os.FileInfo, err error) error { + err := filepath.Walk(prepackagedPluginsPath, func(walkPath string, info os.FileInfo, err error) error { fileStorePaths = append(fileStorePaths, walkPath) return nil }) if err != nil { - ch.srv.Log().Error("Failed to walk prepackaged plugins", mlog.Err(err)) - return nil + return errors.Wrap(err, "failed to walk prepackaged plugins") } pluginSignaturePathMap := ch.getPluginsFromFilePaths(fileStorePaths) - plugins := make([]*plugin.PrepackagedPlugin, 0, len(pluginSignaturePathMap)) - prepackagedPlugins := make(chan *plugin.PrepackagedPlugin, len(pluginSignaturePathMap)) + plugins := make(chan *plugin.PrepackagedPlugin, len(pluginSignaturePathMap)) + + // Before processing any prepackaged plugins, take a snapshot of the available manifests + // to decide what was synced from the filestore. + pluginsEnvironment := ch.GetPluginsEnvironment() + if pluginsEnvironment == nil { + return errors.New("pluginsEnvironment is nil") + } + + availablePlugins, err := pluginsEnvironment.Available() + if err != nil { + return errors.Wrap(err, "failed to list available plugins") + } + + availablePluginsMap := make(map[string]*model.BundleInfo, len(availablePlugins)) + for _, bundleInfo := range availablePlugins { + availablePluginsMap[bundleInfo.Manifest.Id] = bundleInfo + } var wg sync.WaitGroup for _, psPath := range pluginSignaturePathMap { @@ -946,18 +966,29 @@ func (ch *Channels) processPrepackagedPlugins(pluginsDir string) []*plugin.Prepa ch.srv.Log().Error("Failed to install prepackaged plugin", mlog.String("bundle_path", psPath.bundlePath), mlog.Err(err)) return } - prepackagedPlugins <- p + + plugins <- p }(psPath) } wg.Wait() - close(prepackagedPlugins) + close(plugins) - for p := range prepackagedPlugins { - plugins = append(plugins, p) + prepackagedPlugins := make([]*plugin.PrepackagedPlugin, 0, len(pluginSignaturePathMap)) + transitionallyPrepackagedPlugins := make([]*plugin.PrepackagedPlugin, 0) + for p := range plugins { + if ch.pluginIsTransitionallyPrepackaged(p.Manifest.Id) { + if ch.shouldPersistTransitionallyPrepackagedPlugin(availablePluginsMap, p) { + transitionallyPrepackagedPlugins = append(transitionallyPrepackagedPlugins, p) + } + } else { + prepackagedPlugins = append(prepackagedPlugins, p) + } } - return plugins + pluginsEnvironment.SetPrepackagedPlugins(prepackagedPlugins, transitionallyPrepackagedPlugins) + + return nil } // processPrepackagedPlugin will return the prepackaged plugin metadata and will also @@ -984,25 +1015,172 @@ func (ch *Channels) processPrepackagedPlugin(pluginPath *pluginSignaturePath) (* return nil, errors.Wrapf(err, "Failed to get prepackaged plugin %s", pluginPath.bundlePath) } + logger = logger.With(mlog.String("plugin_id", plugin.Manifest.Id)) + // Skip installing the plugin at all if automatic prepackaged plugins is disabled if !*ch.cfgSvc.Config().PluginSettings.AutomaticPrepackagedPlugins { + logger.Info("Not installing prepackaged plugin: automatic prepackaged plugins disabled") return plugin, nil } // Skip installing if the plugin is has not been previously enabled. pluginState := ch.cfgSvc.Config().PluginSettings.PluginStates[plugin.Manifest.Id] if pluginState == nil || !pluginState.Enable { + logger.Info("Not installing prepackaged plugin: not previously enabled") return plugin, nil } - logger.Info("Installing prepackaged plugin") - if _, err := ch.installExtractedPlugin(plugin.Manifest, pluginDir, installPluginLocallyOnlyIfNewOrUpgrade); err != nil { + if _, err := ch.installExtractedPlugin(plugin.Manifest, pluginDir, installPluginLocallyOnlyIfNewOrUpgrade); err != nil && err.Id != "app.plugin.skip_installation.app_error" { return nil, errors.Wrapf(err, "Failed to install extracted prepackaged plugin %s", pluginPath.bundlePath) } return plugin, nil } +var transitionallyPrepackagedPlugins = []string{ + "antivirus", + "focalboard", + "mattermost-autolink", + "com.mattermost.aws-sns", + "com.mattermost.plugin-channel-export", + "com.mattermost.confluence", + "com.mattermost.custom-attributes", + "jenkins", + "jitsi", + "com.mattermost.plugin-todo", + "com.mattermost.welcomebot", + "com.mattermost.apps", +} + +// pluginIsTransitionallyPrepackaged identifies plugin ids that are currently prepackaged but +// slated for future removal. +func (ch *Channels) pluginIsTransitionallyPrepackaged(pluginID string) bool { + for _, id := range transitionallyPrepackagedPlugins { + if id == pluginID { + return true + } + } + + return false +} + +// shouldPersistTransitionallyPrepackagedPlugin determines if a transitionally prepackaged plugin +// should be persisted to the filestore, taking into account whether it's already enabled and +// would improve on what's already in the filestore. +func (ch *Channels) shouldPersistTransitionallyPrepackagedPlugin(availablePluginsMap map[string]*model.BundleInfo, p *plugin.PrepackagedPlugin) bool { + logger := ch.srv.Log().With(mlog.String("plugin_id", p.Manifest.Id), mlog.String("prepackaged_version", p.Manifest.Version)) + + // Ignore the plugin altogether unless it was previously enabled. + pluginState := ch.cfgSvc.Config().PluginSettings.PluginStates[p.Manifest.Id] + if pluginState == nil || !pluginState.Enable { + logger.Debug("Should not persist transitionally prepackaged plugin: not previously enabled") + return false + } + + // Ignore the plugin if the same or newer version is already available + // (having previously synced from the filestore). + existing, found := availablePluginsMap[p.Manifest.Id] + if !found { + logger.Info("Should persist transitionally prepackaged plugin: not currently in filestore") + return true + } + + prepackagedVersion, err := semver.Parse(p.Manifest.Version) + if err != nil { + logger.Error("Should not persist transitionally prepackged plugin: invalid prepackaged version", mlog.Err(err)) + return false + } + + logger = logger.With(mlog.String("existing_version", existing.Manifest.Version)) + + existingVersion, err := semver.Parse(existing.Manifest.Version) + if err != nil { + // Consider this an old version and replace with the prepackaged version instead. + logger.Warn("Should persist transitionally prepackged plugin: invalid existing version", mlog.Err(err)) + return true + } + + if prepackagedVersion.GT(existingVersion) { + logger.Info("Should persist transitionally prepackged plugin: newer version") + return true + } + + logger.Info("Should not persist transitionally prepackged plugin: not a newer version") + return false +} + +// persistTransitionallyPrepackagedPlugins writes plugins that are transitionally prepackaged with +// the server to the filestore to allow their continued use when the plugin eventually stops being +// prepackaged. +// +// We identify which plugins need to be persisted during startup via processPrepackagedPlugins. +// Once we persist the set of plugins to the filestore, we clear the list to prevent this server +// from trying again. +// +// In a multi-server cluster, only the cluster leader should persist these plugins to avoid +// concurrent writes to the filestore. But during an upgrade, there's no guarantee that a freshly +// upgraded server will be the cluster leader to perform this step in a timely fashion, so the +// persistence has to be able to happen sometime after startup. Additionally, while this is a +// kind of migration, it's not a one off: new versions of these plugins may still be shipped +// during the transition period, or new plugins may be added to the list. +// +// So instead of a one-time migration, we opt to run this method every time the cluster leader +// changes, but minimizing rework. More than one server may end up persisting the same plugin +// (but never concurrently!), but all servers will eventually converge on this method becoming a +// no-op (until this set of plugins changes in a subsequent release). +// +// Finally, if an error occurs persisting the plugin, we don't try again until the server restarts, +// or another server becomes cluster leader. +func (ch *Channels) persistTransitionallyPrepackagedPlugins() { + if !ch.srv.IsLeader() { + ch.srv.Log().Debug("Not persisting transitionally prepackaged plugins: not the leader") + return + } + + pluginsEnvironment := ch.GetPluginsEnvironment() + if pluginsEnvironment == nil { + ch.srv.Log().Debug("Not persisting transitionally prepackaged plugins: no plugin environment") + return + } + + transitionallyPrepackagedPlugins := pluginsEnvironment.TransitionallyPrepackagedPlugins() + if len(transitionallyPrepackagedPlugins) == 0 { + ch.srv.Log().Debug("Not persisting transitionally prepackaged plugins: none found") + return + } + + var wg sync.WaitGroup + for _, p := range transitionallyPrepackagedPlugins { + wg.Add(1) + go func(p *plugin.PrepackagedPlugin) { + defer wg.Done() + + logger := ch.srv.Log().With(mlog.String("plugin_id", p.Manifest.Id), mlog.String("version", p.Manifest.Version)) + + logger.Info("Persisting transitionally prepackaged plugin") + + bundleReader, err := os.Open(p.Path) + if err != nil { + logger.Error("Failed to read transitionally prepackaged plugin", mlog.Err(err)) + } + defer bundleReader.Close() + + signatureReader := bytes.NewReader(p.Signature) + + // Write the plugin to the filestore, but don't bother notifying the peers, + // as there's no reason to reload the plugin to run the same version again. + appErr := ch.installPluginToFilestore(p.Manifest, bundleReader, signatureReader) + if appErr != nil { + logger.Error("Failed to persist transitionally prepackaged plugin", mlog.Err(err)) + } + }(p) + } + wg.Wait() + + pluginsEnvironment.ClearTransitionallyPrepackagedPlugins() + ch.srv.Log().Info("Finished persisting transitionally prepackaged plugins") +} + // buildPrepackagedPlugin builds a PrepackagedPlugin from the plugin at the given path, additionally returning the directory in which it was extracted. func (ch *Channels) buildPrepackagedPlugin(pluginPath *pluginSignaturePath, pluginFile io.ReadSeeker, tmpDir string) (*plugin.PrepackagedPlugin, string, error) { manifest, pluginDir, appErr := extractPlugin(pluginFile, tmpDir) diff --git a/server/channels/app/plugin_api.go b/server/channels/app/plugin_api.go index 9f373683568..7c6f38cd3e8 100644 --- a/server/channels/app/plugin_api.go +++ b/server/channels/app/plugin_api.go @@ -17,7 +17,7 @@ import ( "github.com/mattermost/mattermost/server/public/model" "github.com/mattermost/mattermost/server/public/shared/i18n" "github.com/mattermost/mattermost/server/public/shared/mlog" - "github.com/mattermost/mattermost/server/v8/channels/app/request" + "github.com/mattermost/mattermost/server/public/shared/request" ) type PluginAPI struct { @@ -257,6 +257,10 @@ func (api *PluginAPI) GetUserByUsername(name string) (*model.User, *model.AppErr return api.app.GetUserByUsername(name) } +func (api *PluginAPI) GetUserByRemoteID(remoteID string) (*model.User, *model.AppError) { + return api.app.GetUserByRemoteID(remoteID) +} + func (api *PluginAPI) GetUsersByUsernames(usernames []string) ([]*model.User, *model.AppError) { return api.app.GetUsersByUsernames(usernames, true, nil) } @@ -762,6 +766,10 @@ func (api *PluginAPI) GetFileInfo(fileID string) (*model.FileInfo, *model.AppErr return api.app.GetFileInfo(fileID) } +func (api *PluginAPI) SetFileSearchableContent(fileID string, content string) *model.AppError { + return api.app.SetFileSearchableContent(fileID, content) +} + func (api *PluginAPI) GetFileInfos(page, perPage int, opt *model.GetFileInfosOptions) ([]*model.FileInfo, *model.AppError) { return api.app.GetFileInfos(page, perPage, opt) } @@ -1262,3 +1270,8 @@ func (api *PluginAPI) GetUploadSession(uploadID string) (*model.UploadSession, e } return fi, nil } + +func (api *PluginAPI) SendPushNotification(notification *model.PushNotification, userID string) *model.AppError { + // Ignoring skipSessionId because it's only used internally to clear push notifications + return api.app.sendPushNotificationToAllSessions(notification, userID, "") +} diff --git a/server/channels/app/plugin_api_test.go b/server/channels/app/plugin_api_test.go index 056546de85c..d2270687648 100644 --- a/server/channels/app/plugin_api_test.go +++ b/server/channels/app/plugin_api_test.go @@ -19,6 +19,7 @@ import ( "path" "path/filepath" "strings" + "sync" "testing" "time" @@ -30,7 +31,7 @@ import ( "github.com/mattermost/mattermost/server/public/plugin" "github.com/mattermost/mattermost/server/public/plugin/utils" "github.com/mattermost/mattermost/server/public/shared/i18n" - "github.com/mattermost/mattermost/server/v8/channels/app/request" + "github.com/mattermost/mattermost/server/public/shared/request" "github.com/mattermost/mattermost/server/v8/channels/utils/fileutils" "github.com/mattermost/mattermost/server/v8/einterfaces/mocks" ) @@ -178,21 +179,22 @@ func TestPluginAPIGetUserPreferences(t *testing.T) { preferences, err := api.GetPreferencesForUser(user1.Id) require.Nil(t, err) - assert.Equal(t, 2, len(preferences)) + assert.Equal(t, 3, len(preferences)) assert.Equal(t, user1.Id, preferences[0].UserId) assert.Equal(t, model.PreferenceRecommendedNextSteps, preferences[0].Category) assert.Equal(t, "hide", preferences[0].Name) assert.Equal(t, "false", preferences[0].Value) - assert.Equal(t, user1.Id, preferences[1].UserId) - assert.Equal(t, model.PreferenceCategoryTutorialSteps, preferences[1].Category) - assert.Equal(t, user1.Id, preferences[1].Name) - assert.Equal(t, "0", preferences[1].Value) + assert.Equal(t, model.PreferenceCategorySystemNotice, preferences[1].Category) + + assert.Equal(t, user1.Id, preferences[2].UserId) + assert.Equal(t, model.PreferenceCategoryTutorialSteps, preferences[2].Category) + assert.Equal(t, user1.Id, preferences[2].Name) + assert.Equal(t, "0", preferences[2].Value) } func TestPluginAPIDeleteUserPreferences(t *testing.T) { - t.Skip("MM-47612") th := Setup(t) defer th.TearDown() api := th.SetupPluginAPI() @@ -240,9 +242,11 @@ func TestPluginAPIDeleteUserPreferences(t *testing.T) { require.Nil(t, err) preferences, err = api.GetPreferencesForUser(user2.Id) require.Nil(t, err) - assert.Equal(t, 2, len(preferences)) - assert.Equal(t, model.PreferenceRecommendedNextSteps, preferences[0].Category) - assert.Equal(t, model.PreferenceCategoryTutorialSteps, preferences[1].Category) + assert.Equal(t, 3, len(preferences)) + assert.ElementsMatch(t, + []string{model.PreferenceRecommendedNextSteps, model.PreferenceCategoryTutorialSteps, model.PreferenceCategorySystemNotice}, + []string{preferences[0].Category, preferences[1].Category, preferences[2].Category}, + ) } func TestPluginAPIUpdateUserPreferences(t *testing.T) { @@ -260,16 +264,17 @@ func TestPluginAPIUpdateUserPreferences(t *testing.T) { preferences, err := api.GetPreferencesForUser(user1.Id) require.Nil(t, err) - assert.Equal(t, 2, len(preferences)) + assert.Equal(t, 3, len(preferences)) assert.Equal(t, user1.Id, preferences[0].UserId) assert.Equal(t, model.PreferenceRecommendedNextSteps, preferences[0].Category) assert.Equal(t, "hide", preferences[0].Name) assert.Equal(t, "false", preferences[0].Value) - assert.Equal(t, user1.Id, preferences[1].UserId) - assert.Equal(t, model.PreferenceCategoryTutorialSteps, preferences[1].Category) - assert.Equal(t, user1.Id, preferences[1].Name) - assert.Equal(t, "0", preferences[1].Value) + assert.Equal(t, model.PreferenceCategorySystemNotice, preferences[1].Category) + assert.Equal(t, user1.Id, preferences[2].UserId) + assert.Equal(t, model.PreferenceCategoryTutorialSteps, preferences[2].Category) + assert.Equal(t, user1.Id, preferences[2].Name) + assert.Equal(t, "0", preferences[2].Value) preference := model.Preference{ Name: user1.Id, @@ -284,8 +289,8 @@ func TestPluginAPIUpdateUserPreferences(t *testing.T) { preferences, err = api.GetPreferencesForUser(user1.Id) require.Nil(t, err) - assert.Equal(t, 3, len(preferences)) - expectedCategories := []string{model.PreferenceCategoryTutorialSteps, model.PreferenceCategoryTheme, model.PreferenceRecommendedNextSteps} + assert.Equal(t, 4, len(preferences)) + expectedCategories := []string{model.PreferenceCategoryTutorialSteps, model.PreferenceCategoryTheme, model.PreferenceRecommendedNextSteps, model.PreferenceCategorySystemNotice} for _, pref := range preferences { assert.Contains(t, expectedCategories, pref.Category) assert.Equal(t, user1.Id, pref.UserId) @@ -2205,6 +2210,103 @@ func TestConfigurationWillBeSavedHook(t *testing.T) { }) } +func TestSendPushNotification(t *testing.T) { + if testing.Short() { + t.Skip("skipping TestSendPushNotification test in short mode") + } + + th := Setup(t).InitBasic() + defer th.TearDown() + api := th.SetupPluginAPI() + + // Create 3 users, each having 2 sessions. + type userSession struct { + user *model.User + session *model.Session + } + var userSessions []userSession + for i := 0; i < 3; i++ { + u := th.CreateUser() + sess, err := th.App.CreateSession(&model.Session{ + UserId: u.Id, + DeviceId: "deviceID" + u.Id, + ExpiresAt: model.GetMillis() + 100000, + }) + require.Nil(t, err) + // We don't need to track the 2nd session. + _, err = th.App.CreateSession(&model.Session{ + UserId: u.Id, + DeviceId: "deviceID" + u.Id, + ExpiresAt: model.GetMillis() + 100000, + }) + require.Nil(t, err) + _, err = th.App.AddTeamMember(th.Context, th.BasicTeam.Id, u.Id) + require.Nil(t, err) + th.AddUserToChannel(u, th.BasicChannel) + userSessions = append(userSessions, userSession{ + user: u, + session: sess, + }) + } + + handler := &testPushNotificationHandler{ + t: t, + behavior: "simple", + } + pushServer := httptest.NewServer( + http.HandlerFunc(handler.handleReq), + ) + defer pushServer.Close() + + th.App.UpdateConfig(func(cfg *model.Config) { + *cfg.EmailSettings.PushNotificationContents = model.FullNotification + *cfg.EmailSettings.PushNotificationServer = pushServer.URL + }) + + var wg sync.WaitGroup + for _, data := range userSessions { + wg.Add(1) + go func(user model.User) { + defer wg.Done() + post := th.CreatePost(th.BasicChannel) + post.Message = "started a conversation" + notification := &model.PushNotification{ + Category: model.CategoryCanReply, + Version: model.PushMessageV2, + Type: model.PushTypeMessage, + TeamId: th.BasicChannel.TeamId, + ChannelId: th.BasicChannel.Id, + PostId: post.Id, + RootId: post.RootId, + SenderId: post.UserId, + SenderName: "Sender Name", + PostType: post.Type, + ChannelType: th.BasicChannel.Type, + Message: "Custom message", + } + appErr := api.SendPushNotification(notification, user.Id) + require.Nil(t, appErr) + }(*data.user) + } + wg.Wait() + + // Hack to let the worker goroutines complete. + time.Sleep(1 * time.Second) + // Server side verification. + var numMessages int + for _, n := range handler.notifications() { + switch n.Type { + case model.PushTypeMessage: + numMessages++ + assert.Equal(t, th.BasicChannel.Id, n.ChannelId) + assert.Equal(t, "Custom message", n.Message) + default: + assert.Fail(t, "should not receive any other push notification types") + } + } + assert.Equal(t, 6, numMessages) +} + func TestPluginServeMetrics(t *testing.T) { th := Setup(t, StartMetrics) defer th.TearDown() diff --git a/server/channels/app/plugin_api_tests/test_db_driver/main.go b/server/channels/app/plugin_api_tests/test_db_driver/main.go index 731fb0fc746..bbf50f4267b 100644 --- a/server/channels/app/plugin_api_tests/test_db_driver/main.go +++ b/server/channels/app/plugin_api_tests/test_db_driver/main.go @@ -9,10 +9,10 @@ import ( "github.com/mattermost/mattermost/server/public/model" "github.com/mattermost/mattermost/server/public/plugin" + "github.com/mattermost/mattermost/server/public/shared/driver" "github.com/mattermost/mattermost/server/v8/channels/app/plugin_api_tests" "github.com/mattermost/mattermost/server/v8/channels/store/sqlstore" "github.com/mattermost/mattermost/server/v8/channels/store/storetest" - "github.com/mattermost/mattermost/server/v8/platform/shared/driver" ) type MyPlugin struct { diff --git a/server/channels/app/plugin_commands.go b/server/channels/app/plugin_commands.go index 17b246fa7dd..24c64e2c800 100644 --- a/server/channels/app/plugin_commands.go +++ b/server/channels/app/plugin_commands.go @@ -13,7 +13,7 @@ import ( "github.com/mattermost/mattermost/server/public/model" "github.com/mattermost/mattermost/server/public/shared/mlog" - "github.com/mattermost/mattermost/server/v8/channels/app/request" + "github.com/mattermost/mattermost/server/public/shared/request" ) type PluginCommand struct { diff --git a/server/channels/app/plugin_commands_test.go b/server/channels/app/plugin_commands_test.go index dee14b35b81..015d1bfd2d6 100644 --- a/server/channels/app/plugin_commands_test.go +++ b/server/channels/app/plugin_commands_test.go @@ -13,7 +13,7 @@ import ( "github.com/mattermost/mattermost/server/public/model" "github.com/mattermost/mattermost/server/public/plugin" "github.com/mattermost/mattermost/server/public/shared/i18n" - "github.com/mattermost/mattermost/server/v8/channels/app/request" + "github.com/mattermost/mattermost/server/public/shared/request" "github.com/mattermost/mattermost/server/v8/channels/product" ) diff --git a/server/channels/app/plugin_hooks_test.go b/server/channels/app/plugin_hooks_test.go index 7657ead163a..02e8bf2e01f 100644 --- a/server/channels/app/plugin_hooks_test.go +++ b/server/channels/app/plugin_hooks_test.go @@ -5,12 +5,14 @@ package app import ( "bytes" - "context" + _ "embed" + "fmt" "io" "net/http" "net/http/httptest" "os" "path/filepath" + "sync" "testing" "time" @@ -23,7 +25,7 @@ import ( "github.com/mattermost/mattermost/server/public/plugin" "github.com/mattermost/mattermost/server/public/plugin/plugintest" "github.com/mattermost/mattermost/server/public/plugin/utils" - "github.com/mattermost/mattermost/server/v8/channels/app/request" + "github.com/mattermost/mattermost/server/public/shared/request" "github.com/mattermost/mattermost/server/v8/einterfaces/mocks" ) @@ -427,6 +429,50 @@ func TestHookMessageHasBeenUpdated(t *testing.T) { require.Nil(t, err) } +func TestHookMessageHasBeenDeleted(t *testing.T) { + th := Setup(t).InitBasic() + defer th.TearDown() + + var mockAPI plugintest.API + mockAPI.On("LoadPluginConfiguration", mock.Anything).Return(nil) + mockAPI.On("LogDebug", "message").Return(nil).Times(1) + + tearDown, _, _ := SetAppEnvironmentWithPlugins(t, + []string{ + ` + package main + + import ( + "github.com/mattermost/mattermost/server/public/plugin" + "github.com/mattermost/mattermost/server/public/model" + ) + + type MyPlugin struct { + plugin.MattermostPlugin + } + + func (p *MyPlugin) MessageHasBeenDeleted(c *plugin.Context, post *model.Post) { + p.API.LogDebug(post.Message) + } + + func main() { + plugin.ClientMain(&MyPlugin{}) + } + `}, th.App, func(*model.Manifest) plugin.API { return &mockAPI }) + defer tearDown() + + post := &model.Post{ + UserId: th.BasicUser.Id, + ChannelId: th.BasicChannel.Id, + Message: "message", + CreateAt: model.GetMillis() - 10000, + } + _, err := th.App.CreatePost(th.Context, post, th.BasicChannel, false, true) + require.Nil(t, err) + _, err = th.App.DeletePost(th.Context, post.Id, th.BasicUser.Id) + require.Nil(t, err) +} + func TestHookFileWillBeUploaded(t *testing.T) { t.Run("rejected", func(t *testing.T) { th := Setup(t).InitBasic() @@ -772,6 +818,55 @@ func TestUserHasLoggedIn(t *testing.T) { assert.Equal(t, user.FirstName, "plugin-callback-success", "Expected firstname overwrite, got default") } +func TestUserHasBeenDeactivated(t *testing.T) { + th := Setup(t) + defer th.TearDown() + + tearDown, _, _ := SetAppEnvironmentWithPlugins(t, + []string{ + ` + package main + + import ( + "github.com/mattermost/mattermost/server/public/plugin" + "github.com/mattermost/mattermost/server/public/model" + ) + + type MyPlugin struct { + plugin.MattermostPlugin + } + + func (p *MyPlugin) UserHasBeenDeactivated(c *plugin.Context, user *model.User) { + user.Nickname = "plugin-callback-success" + p.API.UpdateUser(user) + } + + func main() { + plugin.ClientMain(&MyPlugin{}) + } + `}, th.App, th.NewPluginAPI) + defer tearDown() + + user := &model.User{ + Email: "success+test@example.com", + Nickname: "testnickname", + Username: "testusername", + Password: "testpassword", + } + + _, err := th.App.CreateUser(th.Context, user) + require.Nil(t, err) + + _, err = th.App.UpdateActive(th.Context, user, false) + require.Nil(t, err) + + time.Sleep(1 * time.Second) + user, err = th.App.GetUser(user.Id) + + require.Nil(t, err) + require.Equal(t, "plugin-callback-success", user.Nickname) +} + func TestUserHasBeenCreated(t *testing.T) { th := Setup(t) defer th.TearDown() @@ -802,11 +897,10 @@ func TestUserHasBeenCreated(t *testing.T) { defer tearDown() user := &model.User{ - Email: model.NewId() + "success+test@example.com", - Nickname: "Darth Vader", - Username: "vader" + model.NewId(), - Password: "passwd1", - AuthService: "", + Email: "success+test@example.com", + Nickname: "testnickname", + Username: "testusername", + Password: "testpassword", } _, err := th.App.CreateUser(th.Context, user) require.Nil(t, err) @@ -893,10 +987,9 @@ func TestErrorString(t *testing.T) { func TestHookContext(t *testing.T) { th := Setup(t).InitBasic() defer th.TearDown() + ctx := request.EmptyContext(th.TestLogger) // We don't actually have a session, we are faking it so just set something arbitrarily - ctx := request.NewContext(context.Background(), model.NewId(), model.NewId(), model.NewId(), model.NewId(), model.NewId(), model.Session{}, nil) - ctx.SetLogger(th.TestLogger) ctx.Session().Id = model.NewId() var mockAPI plugintest.API @@ -988,11 +1081,10 @@ func TestActiveHooks(t *testing.T) { require.True(t, th.App.GetPluginsEnvironment().IsActive(pluginID)) user1 := &model.User{ - Email: model.NewId() + "success+test@example.com", - Nickname: "Darth Vader1", - Username: "vader" + model.NewId(), - Password: "passwd1", - AuthService: "", + Email: "success+test@example.com", + Nickname: "testnickname", + Username: "testusername", + Password: "testpassword", } _, appErr := th.App.CreateUser(th.Context, user1) require.Nil(t, appErr) @@ -1094,10 +1186,10 @@ func TestHookMetrics(t *testing.T) { require.True(t, th.App.GetPluginsEnvironment().IsActive(pluginID)) user1 := &model.User{ - Email: model.NewId() + "success+test@example.com", - Nickname: "Darth Vader1", - Username: "vader" + model.NewId(), - Password: "passwd1", + Email: "success+test@example.com", + Nickname: "testnickname", + Username: "testusername", + Password: "testpassword", AuthService: "", } _, appErr := th.App.CreateUser(th.Context, user1) @@ -1331,3 +1423,130 @@ func TestHookOnCloudLimitsUpdated(t *testing.T) { require.True(t, hookCalled) } + +//go:embed test_templates/hook_notification_will_be_pushed.tmpl +var hookNotificationWillBePushedTmpl string + +func TestHookNotificationWillBePushed(t *testing.T) { + if testing.Short() { + t.Skip("skipping TestHookNotificationWillBePushed test in short mode") + } + + tests := []struct { + name string + testCode string + expectedNotifications int + expectedNotificationMessage string + }{ + { + name: "successfully pushed", + testCode: `return nil, ""`, + expectedNotifications: 6, + }, + { + name: "push notification rejected", + testCode: `return nil, "rejected"`, + expectedNotifications: 0, + }, + { + name: "push notification modified", + testCode: `notification.Message = "brand new message" + return notification, ""`, + expectedNotifications: 6, + expectedNotificationMessage: "brand new message", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + th := Setup(t).InitBasic() + defer th.TearDown() + + templatedPlugin := fmt.Sprintf(hookNotificationWillBePushedTmpl, tt.testCode) + tearDown, _, _ := SetAppEnvironmentWithPlugins(t, []string{templatedPlugin}, th.App, th.NewPluginAPI) + defer tearDown() + + // Create 3 users, each having 2 sessions. + type userSession struct { + user *model.User + session *model.Session + } + var userSessions []userSession + for i := 0; i < 3; i++ { + u := th.CreateUser() + sess, err := th.App.CreateSession(&model.Session{ + UserId: u.Id, + DeviceId: "deviceID" + u.Id, + ExpiresAt: model.GetMillis() + 100000, + }) + require.Nil(t, err) + // We don't need to track the 2nd session. + _, err = th.App.CreateSession(&model.Session{ + UserId: u.Id, + DeviceId: "deviceID" + u.Id, + ExpiresAt: model.GetMillis() + 100000, + }) + require.Nil(t, err) + _, err = th.App.AddTeamMember(th.Context, th.BasicTeam.Id, u.Id) + require.Nil(t, err) + th.AddUserToChannel(u, th.BasicChannel) + userSessions = append(userSessions, userSession{ + user: u, + session: sess, + }) + } + + handler := &testPushNotificationHandler{ + t: t, + behavior: "simple", + } + pushServer := httptest.NewServer( + http.HandlerFunc(handler.handleReq), + ) + defer pushServer.Close() + + th.App.UpdateConfig(func(cfg *model.Config) { + *cfg.EmailSettings.PushNotificationContents = model.GenericNotification + *cfg.EmailSettings.PushNotificationServer = pushServer.URL + }) + + var wg sync.WaitGroup + for _, data := range userSessions { + wg.Add(1) + go func(user model.User) { + defer wg.Done() + notification := &PostNotification{ + Post: th.CreatePost(th.BasicChannel), + Channel: th.BasicChannel, + ProfileMap: map[string]*model.User{ + user.Id: &user, + }, + Sender: &user, + } + th.App.sendPushNotification(notification, &user, true, false, model.CommentsNotifyAny) + }(*data.user) + } + wg.Wait() + + // Hack to let the worker goroutines complete. + time.Sleep(1 * time.Second) + // Server side verification. + assert.Equal(t, tt.expectedNotifications, handler.numReqs()) + var numMessages int + for _, n := range handler.notifications() { + switch n.Type { + case model.PushTypeMessage: + numMessages++ + assert.Equal(t, th.BasicChannel.Id, n.ChannelId) + if tt.expectedNotificationMessage != "" { + assert.Equal(t, tt.expectedNotificationMessage, n.Message) + } else { + assert.Contains(t, n.Message, "mentioned you") + } + default: + assert.Fail(t, "should not receive any other push notification types") + } + } + assert.Equal(t, tt.expectedNotifications, numMessages) + }) + } +} diff --git a/server/channels/app/plugin_install.go b/server/channels/app/plugin_install.go index f1967188cea..f27e109e1e2 100644 --- a/server/channels/app/plugin_install.go +++ b/server/channels/app/plugin_install.go @@ -1,38 +1,85 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -// Installing a managed plugin consists of copying the uploaded plugin (*.tar.gz) to the filestore, -// unpacking to the configured local directory (PluginSettings.Directory), and copying any webapp bundle therein -// to the configured local client directory (PluginSettings.ClientDirectory). The unpacking and copy occurs -// each time the server starts, ensuring it remains synchronized with the set of installed plugins. +// ### Plugin Bundles // -// When a plugin is enabled, all connected websocket clients are notified so as to fetch any webapp bundle and -// load the client-side portion of the plugin. This works well in a single-server system, but requires careful -// coordination in a high-availability cluster with multiple servers. In particular, websocket clients must not be -// notified of the newly enabled plugin until all servers in the cluster have finished unpacking the plugin, otherwise -// the webapp bundle might not yet be available. Ideally, each server would just notify its own set of connected peers -// after it finishes this process, but nothing prevents those clients from re-connecting to a different server behind -// the load balancer that hasn't finished unpacking. +// A plugin bundle consists of a server and/or webapp component designed to extend the +// functionality of the server. Bundles are first extracted to the configured local directory +// (PluginSettings.Directory), with any webapp component additionally copied to the configured +// local client directory (PluginSettings.ClientDirectory) to be loaded alongside the webapp. // -// To achieve this coordination, each server instead checks the status of its peers after unpacking. If it finds peers with -// differing versions of the plugin, it skips the notification. If it finds all peers with the same version of the plugin, -// it notifies all websocket clients connected to all peers. There's a small chance that this never occurs if the last -// server to finish unpacking dies before it can announce. There is also a chance that multiple servers decide to notify, -// but the webapp handles this idempotently. +// Plugin bundles are sourced in one of three ways: +// - plugins prepackged with the server in the prepackaged_plugins/ directory +// - plugins transitionally prepackaged with the server in the prepackaged_plugins/ directory +// - plugins installed to the filestore (amazons3 or local, alongisde files and images) +// - unmanaged plugins manually extracted to the confgured local directory +// ┌────────────────────────────┐ +// │ ┌────────────────────────┐ │ +// │ │prepackaged_plugins/ │ │ +// │ │ prepackaged.tar.gz │ │ +// │ │ transitional.tar.gz │ │ +// │ │ │ │ +// │ └────────────────────────┘ │ +// │ │ │ +// │ ▼ │ +// │ ┌────────────────────────┐ │ +// │ │plugins/ │ │ +// │ │ unmanaged/ │ │ +// │ │ filestore/ │ │ ┌────────────────────────┐ +// │ │ .filestore │ │ │s3://bucket/plugins/ │ +// │ │ prepackaged/ │◀┼───│ filestore.tar.gz │ +// │ │ .filestore │ │ │ transitional.tar.gz │ +// │ │ transitional/ │ │ └────────────────────────┘ +// │ │ .filestore │ │ +// │ └────────────────────────┘ │ +// │ ┌────────┤ +// │ │ server │ +// └───────────────────┴────────┘ // -// Complicating this flow further are the various means of notifying. In addition to websocket events, there are cluster -// messages between peers. There is a cluster message when the config changes and a plugin is enabled or disabled. -// There is a cluster message when installing or uninstalling a plugin. There is a cluster message when peer's plugin change -// its status. And finally the act of notifying websocket clients is propagated itself via a cluster message. +// Prepackaged plugins are bundles shipped alongside the server to simplify installation and +// upgrade. This occurs automatically if configured (PluginSettings.AutomaticPrepackagedPlugins) +// and the plugin is enabled (PluginSettings.PluginStates[plugin_id].Enable), unless a matching or +// newer version of the plugin is already installed. // -// The key methods involved in handling these notifications are notifyPluginEnabled and notifyPluginStatusesChanged. -// Note that none of this complexity applies to single-server systems or to plugins without a webapp bundle. +// Transitionally prepackaged plugins are bundles that will stop being prepackaged in a future +// release. On first startup, they are unpacked just like prepackaged plugins, but also get copied +// to the filestore. On future startups, the server uses the version in the filestore. // -// Finally, in addition to managed plugins, note that there are unmanaged and prepackaged plugins. -// Unmanaged plugins are plugins installed manually to the configured local directory (PluginSettings.Directory). -// Prepackaged plugins are included with the server. They otherwise follow the above flow, except do not get uploaded -// to the filestore. Prepackaged plugins override all other plugins with the same plugin id, but only when the prepackaged -// plugin is newer. Managed plugins unconditionally override unmanaged plugins with the same plugin id. +// Plugins are installed to the filestore when the user installs via the marketplace or manually +// uploads a plugin bundle. (Or because the plugin is transitionally prepackaged). +// +// Unmanaged plugins were manually extracted by into the configured local directory. This legacy +// method of installing plugins is distinguished from other extracted plugins by the absence of a +// flag file (.filestore). Managed plugins unconditionally override unmanaged plugins. A future +// version of Mattermost will likely drop support for unmanaged plugins. +// +// ### Enabling a Plugin +// +// When a plugin is enabled, all connected websocket clients are notified so as to fetch any +// webapp bundle and load the client-side portion of the plugin. This works well in a +// single-server system, but requires careful coordination in a high-availability cluster with +// multiple servers. In particular, websocket clients must not be notified of the newly enabled +// plugin until all servers in the cluster have finished unpacking the plugin, otherwise the +// webapp bundle might not yet be available. Ideally, each server would just notify its own set of +// connected peers after it finishes this process, but nothing prevents those clients from +// re-connecting to a different server behind the load balancer that hasn't finished unpacking. +// +// To achieve this coordination, each server instead checks the status of its peers after +// unpacking. If it finds peers with differing versions of the plugin, it skips the notification. +// If it finds all peers with the same version of the plugin, it notifies all websocket clients +// connected to all peers. There's a small chance that this never occurs if the last server to +// finish unpacking dies before it can announce. There is also a chance that multiple servers +// decide to notify, but the webapp handles this idempotently. +// +// Complicating this flow further are the various means of notifying. In addition to websocket +// events, there are cluster messages between peers. There is a cluster message when the config +// changes and a plugin is enabled or disabled. There is a cluster message when installing or +// uninstalling a plugin. There is a cluster message when a peer's plugin changes its status. And +// finally the act of notifying websocket clients is itself propagated via a cluster message. +// +// The key methods involved in handling these notifications are notifyPluginEnabled and +// notifyPluginStatusesChanged. Note that none of this complexity applies to single-server +// systems or to plugins without a webapp bundle. package app import ( @@ -43,7 +90,7 @@ import ( "os" "path/filepath" - "github.com/blang/semver" + "github.com/blang/semver/v4" "github.com/mattermost/mattermost/server/public/model" "github.com/mattermost/mattermost/server/public/shared/mlog" @@ -91,9 +138,14 @@ func (ch *Channels) installPluginFromClusterMessage(pluginID string) { return } defer signature.Close() + + if err := ch.verifyPlugin(bundle, signature); err != nil { + mlog.Error("Failed to validate plugin signature.", mlog.Err(appErr)) + return + } } - manifest, appErr := ch.installPluginLocally(bundle, signature, installPluginLocallyAlways) + manifest, appErr := ch.installPluginLocally(bundle, installPluginLocallyAlways) if appErr != nil { // A log line already appears if the plugin is on the blocklist or skipped if appErr.Id != "app.plugin.blocked.app_error" && appErr.Id != "app.plugin.skip_installation.app_error" { @@ -144,7 +196,7 @@ func (a *App) InstallPlugin(pluginFile io.ReadSeeker, replace bool) (*model.Mani // // The given installation strategy decides how to handle upgrade scenarios. func (ch *Channels) installPlugin(bundle, signature io.ReadSeeker, installationStrategy pluginInstallationStrategy) (*model.Manifest, *model.AppError) { - manifest, appErr := ch.installPluginLocally(bundle, signature, installationStrategy) + manifest, appErr := ch.installPluginLocally(bundle, installationStrategy) if appErr != nil { return nil, appErr } @@ -161,7 +213,7 @@ func (ch *Channels) installPlugin(bundle, signature io.ReadSeeker, installationS } if err := ch.notifyPluginEnabled(manifest); err != nil { - logger.Warn("Failed notify plugin enabled", mlog.Err(err)) + logger.Warn("Failed to notify plugin enabled", mlog.Err(err)) } if err := ch.notifyPluginStatusesChanged(); err != nil { @@ -174,20 +226,33 @@ func (ch *Channels) installPlugin(bundle, signature io.ReadSeeker, installationS // installPluginToFilestore saves the given plugin bundle (optionally signed) to the filestore, // notifying cluster peers accordingly. func (ch *Channels) installPluginToFilestore(manifest *model.Manifest, bundle, signature io.ReadSeeker) *model.AppError { - if signature != nil { + logger := ch.srv.Log().With(mlog.String("plugin_id", manifest.Id)) + logger.Info("Persisting plugin to filestore") + + if signature == nil { + logger.Warn("No signature when persisting plugin to filestore") + } else { + signatureStorePath := getSignatureStorePath(manifest.Id) _, err := signature.Seek(0, 0) if err != nil { return model.NewAppError("saveSignature", "app.plugin.store_signature.app_error", nil, "", http.StatusInternalServerError).Wrap(err) } - if _, appErr := ch.srv.writeFile(signature, getSignatureStorePath(manifest.Id)); appErr != nil { + logger.Debug("Persisting plugin signature to filestore", mlog.String("path", signatureStorePath)) + if _, appErr := ch.srv.writeFile(signature, signatureStorePath); appErr != nil { return model.NewAppError("saveSignature", "app.plugin.store_signature.app_error", nil, "", http.StatusInternalServerError).Wrap(appErr) } } // Store bundle in the file store to allow access from other servers. - bundle.Seek(0, 0) - if _, appErr := ch.srv.writeFile(bundle, getBundleStorePath(manifest.Id)); appErr != nil { + bundleStorePath := getBundleStorePath(manifest.Id) + _, err := bundle.Seek(0, 0) + if err != nil { + return model.NewAppError("uploadPlugin", "app.plugin.store_bundle.app_error", nil, "", http.StatusInternalServerError).Wrap(err) + } + + logger.Debug("Persisting plugin bundle to filestore", mlog.String("path", bundleStorePath)) + if _, appErr := ch.srv.writeFile(bundle, bundleStorePath); appErr != nil { return model.NewAppError("uploadPlugin", "app.plugin.store_bundle.app_error", nil, "", http.StatusInternalServerError).Wrap(appErr) } @@ -269,6 +334,11 @@ func (ch *Channels) InstallMarketplacePlugin(request *model.InstallMarketplacePl return nil, model.NewAppError("InstallMarketplacePlugin", "app.plugin.marketplace_plugins.signature_not_found.app_error", nil, "", http.StatusInternalServerError) } + appErr = ch.verifyPlugin(pluginFile, signatureFile) + if appErr != nil { + return nil, appErr + } + manifest, appErr := ch.installPlugin(pluginFile, signatureFile, installPluginLocallyAlways) if appErr != nil { return nil, appErr @@ -288,23 +358,16 @@ const ( installPluginLocallyAlways ) -// installPluginLocally extracts and installs the given plugin bundle (optionally signed) for the -// current server, activating the plugin if already enabled. +// installPluginLocally extracts and installs the given plugin bundle for the current server, +// activating the plugin if already enabled. // // The given installation strategy decides how to handle upgrade scenarios. -func (ch *Channels) installPluginLocally(bundle, signature io.ReadSeeker, installationStrategy pluginInstallationStrategy) (*model.Manifest, *model.AppError) { +func (ch *Channels) installPluginLocally(bundle io.ReadSeeker, installationStrategy pluginInstallationStrategy) (*model.Manifest, *model.AppError) { pluginsEnvironment := ch.GetPluginsEnvironment() if pluginsEnvironment == nil { return nil, model.NewAppError("installPluginLocally", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented) } - // verify signature - if signature != nil { - if err := ch.verifyPlugin(bundle, signature); err != nil { - return nil, err - } - } - tmpDir, err := os.MkdirTemp("", "plugintmp") if err != nil { return nil, model.NewAppError("installPluginLocally", "app.plugin.filesystem.app_error", nil, "", http.StatusInternalServerError).Wrap(err) diff --git a/server/channels/app/plugin_install_test.go b/server/channels/app/plugin_install_test.go index c3e0b31c919..d25e41bd43b 100644 --- a/server/channels/app/plugin_install_test.go +++ b/server/channels/app/plugin_install_test.go @@ -73,7 +73,7 @@ func TestInstallPluginLocally(t *testing.T) { th := Setup(t) defer th.TearDown() - actualManifest, appErr := th.App.ch.installPluginLocally(&nilReadSeeker{}, nil, installPluginLocallyOnlyIfNew) + actualManifest, appErr := th.App.ch.installPluginLocally(&nilReadSeeker{}, installPluginLocallyOnlyIfNew) require.NotNil(t, appErr) assert.Equal(t, "app.plugin.extract.app_error", appErr.Id, appErr.Error()) require.Nil(t, actualManifest) @@ -87,7 +87,7 @@ func TestInstallPluginLocally(t *testing.T) { {"test", "test file"}, }) - actualManifest, appErr := th.App.ch.installPluginLocally(reader, nil, installPluginLocallyOnlyIfNew) + actualManifest, appErr := th.App.ch.installPluginLocally(reader, installPluginLocallyOnlyIfNew) require.NotNil(t, appErr) assert.Equal(t, "app.plugin.manifest.app_error", appErr.Id, appErr.Error()) require.Nil(t, actualManifest) @@ -106,7 +106,7 @@ func TestInstallPluginLocally(t *testing.T) { {"plugin.json", string(manifestJSON)}, }) - actualManifest, appError := th.App.ch.installPluginLocally(reader, nil, installationStrategy) + actualManifest, appError := th.App.ch.installPluginLocally(reader, installationStrategy) if actualManifest != nil { require.Equal(t, manifest, actualManifest) } diff --git a/server/channels/app/plugin_signature.go b/server/channels/app/plugin_signature.go index ad90cf3faad..9fefed3b6b1 100644 --- a/server/channels/app/plugin_signature.go +++ b/server/channels/app/plugin_signature.go @@ -15,6 +15,8 @@ import ( "github.com/mattermost/mattermost/server/public/model" "github.com/mattermost/mattermost/server/public/shared/mlog" + pUtils "github.com/mattermost/mattermost/server/public/utils" + "github.com/mattermost/mattermost/server/v8/channels/utils" ) @@ -46,7 +48,7 @@ func (a *App) AddPublicKey(name string, key io.Reader) *model.AppError { } a.UpdateConfig(func(cfg *model.Config) { - if !utils.StringInSlice(name, cfg.PluginSettings.SignaturePublicKeyFiles) { + if !pUtils.Contains(cfg.PluginSettings.SignaturePublicKeyFiles, name) { cfg.PluginSettings.SignaturePublicKeyFiles = append(cfg.PluginSettings.SignaturePublicKeyFiles, name) } }) diff --git a/server/channels/app/plugin_test.go b/server/channels/app/plugin_test.go index 74be41170e7..b95ae7e9cc6 100644 --- a/server/channels/app/plugin_test.go +++ b/server/channels/app/plugin_test.go @@ -4,15 +4,19 @@ package app import ( + "archive/tar" "bytes" + "compress/gzip" "crypto/sha256" "encoding/base64" + "encoding/json" "fmt" "io" "net/http" "net/http/httptest" "os" "path/filepath" + "sort" "testing" "time" @@ -23,7 +27,7 @@ import ( "github.com/mattermost/mattermost/server/public/model" "github.com/mattermost/mattermost/server/public/plugin" "github.com/mattermost/mattermost/server/public/shared/mlog" - "github.com/mattermost/mattermost/server/v8/channels/app/request" + "github.com/mattermost/mattermost/server/public/shared/request" "github.com/mattermost/mattermost/server/v8/channels/testlib" "github.com/mattermost/mattermost/server/v8/channels/utils/fileutils" ) @@ -805,202 +809,566 @@ func TestPluginStatusActivateError(t *testing.T) { }) } +type byId []*plugin.PrepackagedPlugin + +func (a byId) Len() int { return len(a) } +func (a byId) Swap(i, j int) { a[i], a[j] = a[j], a[i] } +func (a byId) Less(i, j int) bool { return a[i].Manifest.Id < a[j].Manifest.Id } + +type pluginStatusById model.PluginStatuses + +func (a pluginStatusById) Len() int { return len(a) } +func (a pluginStatusById) Swap(i, j int) { a[i], a[j] = a[j], a[i] } +func (a pluginStatusById) Less(i, j int) bool { return a[i].PluginId < a[j].PluginId } + func TestProcessPrepackagedPlugins(t *testing.T) { - th := Setup(t) - defer th.TearDown() - + // Find the tests folder before we change directories to the temporary workspace. testsPath, _ := fileutils.FindDir("tests") - prepackagedPluginsPath := filepath.Join(testsPath, prepackagedPluginsDir) - fileErr := os.Mkdir(prepackagedPluginsPath, os.ModePerm) - require.NoError(t, fileErr) - defer os.RemoveAll(prepackagedPluginsPath) - prepackagedPluginsDir, found := fileutils.FindDir(prepackagedPluginsPath) - require.True(t, found, "failed to find prepackaged plugins directory") + setup := func(t *testing.T) *TestHelper { + t.Helper() - testPluginPath := filepath.Join(testsPath, "testplugin.tar.gz") - fileErr = testlib.CopyFile(testPluginPath, filepath.Join(prepackagedPluginsDir, "testplugin.tar.gz")) - require.NoError(t, fileErr) + th := Setup(t) + t.Cleanup(th.TearDown) - t.Run("automatic, enabled plugin, no signature", func(t *testing.T) { - // Install the plugin and enable - pluginBytes, err := os.ReadFile(testPluginPath) + wd, err := os.Getwd() require.NoError(t, err) - require.NotNil(t, pluginBytes) + t.Cleanup(func() { + err = os.Chdir(wd) + require.NoError(t, err) + }) - manifest, appErr := th.App.ch.installPluginLocally(bytes.NewReader(pluginBytes), nil, installPluginLocallyAlways) - require.Nil(t, appErr) - require.Equal(t, "testplugin", manifest.Id) + err = os.Chdir(th.tempWorkspace) + require.NoError(t, err) - env := th.App.GetPluginsEnvironment() - - activatedManifest, activated, err := env.Activate(manifest.Id) + // Make a prepackaged_plugins directory for use with the tests. + err = os.Mkdir(prepackagedPluginsDir, os.ModePerm) require.NoError(t, err) - require.True(t, activated) - require.Equal(t, manifest, activatedManifest) th.App.UpdateConfig(func(cfg *model.Config) { *cfg.PluginSettings.Enable = true - *cfg.PluginSettings.AutomaticPrepackagedPlugins = true *cfg.PluginSettings.EnableRemoteMarketplace = false }) - plugins := th.App.ch.processPrepackagedPlugins(prepackagedPluginsDir) - require.Len(t, plugins, 1) - require.Equal(t, plugins[0].Manifest.Id, "testplugin") - require.Empty(t, plugins[0].Signature, 0) + return th + } + + initPlugins := func(t *testing.T, th *TestHelper) ([]*plugin.PrepackagedPlugin, []*plugin.PrepackagedPlugin) { + t.Helper() + + appErr := th.App.ch.syncPlugins() + require.Nil(t, appErr) + + err := th.App.ch.processPrepackagedPlugins(prepackagedPluginsDir) + require.NoError(t, err) + + env := th.App.GetPluginsEnvironment() + plugins, transitionalPlugins := env.PrepackagedPlugins(), env.TransitionallyPrepackagedPlugins() + th.App.ch.persistTransitionallyPrepackagedPlugins() + + return plugins, transitionalPlugins + } + + copyAsPrepackagedPlugin := func(t *testing.T, filename string) { + t.Helper() + + err := testlib.CopyFile(filepath.Join(testsPath, filename), filepath.Join(prepackagedPluginsDir, filename)) + require.NoError(t, err) + + err = testlib.CopyFile(filepath.Join(testsPath, fmt.Sprintf("%s.sig", filename)), filepath.Join(prepackagedPluginsDir, fmt.Sprintf("%s.sig", filename))) + require.NoError(t, err) + } + + copyAsFilestorePlugin := func(t *testing.T, bundleFilename, pluginID string) { + t.Helper() + + err := testlib.CopyFile(filepath.Join(testsPath, bundleFilename), filepath.Join(fmt.Sprintf("data/plugins/%s.tar.gz", pluginID))) + require.NoError(t, err) + err = testlib.CopyFile(filepath.Join(testsPath, fmt.Sprintf("%s.sig", bundleFilename)), filepath.Join(fmt.Sprintf("data/plugins/%s.tar.gz.sig", pluginID))) + require.NoError(t, err) + } + + expectPrepackagedPlugin := func(t *testing.T, pluginID, version string, actual *plugin.PrepackagedPlugin) { + t.Helper() + + require.Equal(t, pluginID, actual.Manifest.Id) + require.NotEmpty(t, actual.Signature, "testplugin has no signature") + require.Equal(t, version, actual.Manifest.Version) + } + + expectPluginInFilestore := func(t *testing.T, pluginID, version string) { + t.Helper() + + bundlePath := fmt.Sprintf("data/plugins/%s.tar.gz", pluginID) + + require.FileExists(t, bundlePath) + require.FileExists(t, fmt.Sprintf("data/plugins/%s.tar.gz.sig", pluginID)) + + // Verify the version recorded in the Manifest + f, err := os.Open(bundlePath) + require.NoError(t, err) + + uncompressedStream, err := gzip.NewReader(f) + require.NoError(t, err) + + tarReader := tar.NewReader(uncompressedStream) + + var manifest model.Manifest + for { + header, err := tarReader.Next() + if err == io.EOF { + break + } + require.NoError(t, err) + + t.Log(header.Name) + if header.Typeflag == tar.TypeReg && filepath.Base(header.Name) == "plugin.json" { + manifestReader := json.NewDecoder(tarReader) + err = manifestReader.Decode(&manifest) + require.NoError(t, err) + break + } + } + + require.Equal(t, pluginID, manifest.Id, "failed to find manifest") + require.Equal(t, version, manifest.Version) + } + + expectPluginNotInFilestore := func(t *testing.T, pluginID string) { + t.Helper() + + require.NoFileExists(t, fmt.Sprintf("data/plugins/%s.tar.gz", pluginID)) + require.NoFileExists(t, fmt.Sprintf("data/plugins/%s.tar.gz.sig", pluginID)) + } + + expectPluginStatus := func(t *testing.T, pluginID, version string, actual *model.PluginStatus) { + t.Helper() + + require.Equal(t, pluginID, actual.PluginId) + require.Equal(t, version, actual.Version) + } + + t.Run("single plugin automatically installed since enabled", func(t *testing.T) { + th := setup(t) + env := th.App.GetPluginsEnvironment() + + th.App.UpdateConfig(func(cfg *model.Config) { + *cfg.PluginSettings.AutomaticPrepackagedPlugins = true + cfg.PluginSettings.PluginStates["testplugin"] = &model.PluginState{Enable: true} + }) + + copyAsPrepackagedPlugin(t, "testplugin.tar.gz") + + plugins, transitionalPlugins := initPlugins(t, th) + require.Len(t, plugins, 1, "expected one prepackaged plugin") + expectPrepackagedPlugin(t, "testplugin", "0.0.1", plugins[0]) + require.Empty(t, transitionalPlugins) pluginStatus, err := env.Statuses() require.NoError(t, err) require.Len(t, pluginStatus, 1) - require.Equal(t, pluginStatus[0].PluginId, "testplugin") + expectPluginStatus(t, "testplugin", "0.0.1", pluginStatus[0]) - appErr = th.App.ch.RemovePlugin("testplugin") - checkNoError(t, appErr) - - pluginStatus, err = env.Statuses() - require.NoError(t, err) - require.Len(t, pluginStatus, 0) + expectPluginNotInFilestore(t, "testplugin") }) - t.Run("automatic, not enabled plugin", func(t *testing.T) { - th.App.UpdateConfig(func(cfg *model.Config) { - *cfg.PluginSettings.Enable = true - *cfg.PluginSettings.AutomaticPrepackagedPlugins = true - *cfg.PluginSettings.EnableRemoteMarketplace = false - }) - + t.Run("single plugin, not automatically installed since not enabled", func(t *testing.T) { + th := setup(t) env := th.App.GetPluginsEnvironment() - plugins := th.App.ch.processPrepackagedPlugins(prepackagedPluginsDir) - require.Len(t, plugins, 1) - require.Equal(t, plugins[0].Manifest.Id, "testplugin") - require.Empty(t, plugins[0].Signature, 0) + th.App.UpdateConfig(func(cfg *model.Config) { + *cfg.PluginSettings.AutomaticPrepackagedPlugins = true + }) + + copyAsPrepackagedPlugin(t, "testplugin.tar.gz") + + plugins, transitionalPlugins := initPlugins(t, th) + require.Len(t, plugins, 1, "expected one prepackaged plugin") + expectPrepackagedPlugin(t, "testplugin", "0.0.1", plugins[0]) + require.Empty(t, transitionalPlugins) pluginStatus, err := env.Statuses() require.NoError(t, err) require.Empty(t, pluginStatus, 0) + + expectPluginNotInFilestore(t, "testplugin") }) - t.Run("automatic, multiple plugins with signatures, not enabled", func(t *testing.T) { - th.App.UpdateConfig(func(cfg *model.Config) { - *cfg.PluginSettings.Enable = true - *cfg.PluginSettings.AutomaticPrepackagedPlugins = true - *cfg.PluginSettings.EnableRemoteMarketplace = false - }) - + t.Run("single plugin, not automatically installed despite enabled since automatic prepackaged plugins disabled", func(t *testing.T) { + th := setup(t) env := th.App.GetPluginsEnvironment() - // Add signature - testPluginSignaturePath := filepath.Join(testsPath, "testplugin.tar.gz.sig") - err := testlib.CopyFile(testPluginSignaturePath, filepath.Join(prepackagedPluginsDir, "testplugin.tar.gz.sig")) - require.NoError(t, err) + th.App.UpdateConfig(func(cfg *model.Config) { + *cfg.PluginSettings.AutomaticPrepackagedPlugins = false + cfg.PluginSettings.PluginStates["testplugin"] = &model.PluginState{Enable: true} + }) - // Add second plugin - testPlugin2Path := filepath.Join(testsPath, "testplugin2.tar.gz") - err = testlib.CopyFile(testPlugin2Path, filepath.Join(prepackagedPluginsDir, "testplugin2.tar.gz")) - require.NoError(t, err) + copyAsPrepackagedPlugin(t, "testplugin.tar.gz") - testPlugin2SignaturePath := filepath.Join(testsPath, "testplugin2.tar.gz.sig") - err = testlib.CopyFile(testPlugin2SignaturePath, filepath.Join(prepackagedPluginsDir, "testplugin2.tar.gz.sig")) - require.NoError(t, err) - - plugins := th.App.ch.processPrepackagedPlugins(prepackagedPluginsDir) - require.Len(t, plugins, 2) - require.Contains(t, []string{"testplugin", "testplugin2"}, plugins[0].Manifest.Id) - require.NotEmpty(t, plugins[0].Signature) - require.Contains(t, []string{"testplugin", "testplugin2"}, plugins[1].Manifest.Id) - require.NotEmpty(t, plugins[1].Signature) + plugins, transitionalPlugins := initPlugins(t, th) + require.Len(t, plugins, 1, "expected one prepackaged plugin") + expectPrepackagedPlugin(t, "testplugin", "0.0.1", plugins[0]) + require.Empty(t, transitionalPlugins) pluginStatus, err := env.Statuses() require.NoError(t, err) - require.Len(t, pluginStatus, 0) + require.Empty(t, pluginStatus, 0) + + expectPluginNotInFilestore(t, "testplugin") }) - t.Run("automatic, multiple plugins with signatures, one enabled", func(t *testing.T) { - th.App.UpdateConfig(func(cfg *model.Config) { - *cfg.PluginSettings.Enable = true - *cfg.PluginSettings.AutomaticPrepackagedPlugins = true - *cfg.PluginSettings.EnableRemoteMarketplace = false - }) - + t.Run("multiple plugins, some automatically installed since enabled", func(t *testing.T) { + th := setup(t) env := th.App.GetPluginsEnvironment() - // Add signature - testPluginSignaturePath := filepath.Join(testsPath, "testplugin.tar.gz.sig") - err := testlib.CopyFile(testPluginSignaturePath, filepath.Join(prepackagedPluginsDir, "testplugin.tar.gz.sig")) - require.NoError(t, err) + th.App.UpdateConfig(func(cfg *model.Config) { + *cfg.PluginSettings.AutomaticPrepackagedPlugins = true + cfg.PluginSettings.PluginStates["testplugin2"] = &model.PluginState{Enable: true} + }) - // Install first plugin and enable - pluginBytes, err := os.ReadFile(testPluginPath) - require.NoError(t, err) - require.NotNil(t, pluginBytes) + copyAsPrepackagedPlugin(t, "testplugin.tar.gz") + copyAsPrepackagedPlugin(t, "testplugin2.tar.gz") - manifest, appErr := th.App.ch.installPluginLocally(bytes.NewReader(pluginBytes), nil, installPluginLocallyAlways) - require.Nil(t, appErr) - require.Equal(t, "testplugin", manifest.Id) - - activatedManifest, activated, err := env.Activate(manifest.Id) - require.NoError(t, err) - require.True(t, activated) - require.Equal(t, manifest, activatedManifest) - - // Add second plugin - testPlugin2Path := filepath.Join(testsPath, "testplugin2.tar.gz") - err = testlib.CopyFile(testPlugin2Path, filepath.Join(prepackagedPluginsDir, "testplugin2.tar.gz")) - require.NoError(t, err) - - testPlugin2SignaturePath := filepath.Join(testsPath, "testplugin2.tar.gz.sig") - err = testlib.CopyFile(testPlugin2SignaturePath, filepath.Join(prepackagedPluginsDir, "testplugin2.tar.gz.sig")) - require.NoError(t, err) - - plugins := th.App.ch.processPrepackagedPlugins(prepackagedPluginsDir) - require.Len(t, plugins, 2) - require.Contains(t, []string{"testplugin", "testplugin2"}, plugins[0].Manifest.Id) - require.NotEmpty(t, plugins[0].Signature) - require.Contains(t, []string{"testplugin", "testplugin2"}, plugins[1].Manifest.Id) - require.NotEmpty(t, plugins[1].Signature) + plugins, transitionalPlugins := initPlugins(t, th) + require.Len(t, plugins, 2, "expected two prepackaged plugins") + sort.Sort(byId(plugins)) + expectPrepackagedPlugin(t, "testplugin", "0.0.1", plugins[0]) + expectPrepackagedPlugin(t, "testplugin2", "1.2.3", plugins[1]) + require.Empty(t, transitionalPlugins) pluginStatus, err := env.Statuses() require.NoError(t, err) require.Len(t, pluginStatus, 1) - require.Equal(t, pluginStatus[0].PluginId, "testplugin") + expectPluginStatus(t, "testplugin2", "1.2.3", pluginStatus[0]) - appErr = th.App.ch.RemovePlugin("testplugin") - checkNoError(t, appErr) - - pluginStatus, err = env.Statuses() - require.NoError(t, err) - require.Len(t, pluginStatus, 0) + expectPluginNotInFilestore(t, "testplugin") + expectPluginNotInFilestore(t, "testplugin2") }) - t.Run("non-automatic, multiple plugins", func(t *testing.T) { - th.App.UpdateConfig(func(cfg *model.Config) { - *cfg.PluginSettings.Enable = true - *cfg.PluginSettings.AutomaticPrepackagedPlugins = false - *cfg.PluginSettings.EnableRemoteMarketplace = false - }) - + t.Run("multiple plugins, one previously installed, all now installed", func(t *testing.T) { + th := setup(t) env := th.App.GetPluginsEnvironment() - testPlugin2Path := filepath.Join(testsPath, "testplugin2.tar.gz") - err := testlib.CopyFile(testPlugin2Path, filepath.Join(prepackagedPluginsDir, "testplugin2.tar.gz")) + th.App.UpdateConfig(func(cfg *model.Config) { + *cfg.PluginSettings.AutomaticPrepackagedPlugins = true + }) + + copyAsFilestorePlugin(t, "testplugin.tar.gz", "testplugin") + + th.App.UpdateConfig(func(cfg *model.Config) { + cfg.PluginSettings.PluginStates["testplugin"] = &model.PluginState{Enable: true} + cfg.PluginSettings.PluginStates["testplugin2"] = &model.PluginState{Enable: true} + }) + + copyAsPrepackagedPlugin(t, "testplugin.tar.gz") + copyAsPrepackagedPlugin(t, "testplugin2.tar.gz") + + plugins, transitionalPlugins := initPlugins(t, th) + require.Len(t, plugins, 2, "expected two prepackaged plugins") + sort.Sort(byId(plugins)) + expectPrepackagedPlugin(t, "testplugin", "0.0.1", plugins[0]) + expectPrepackagedPlugin(t, "testplugin2", "1.2.3", plugins[1]) + require.Empty(t, transitionalPlugins) + + pluginStatus, err := env.Statuses() require.NoError(t, err) - testPlugin2SignaturePath := filepath.Join(testsPath, "testplugin2.tar.gz.sig") - err = testlib.CopyFile(testPlugin2SignaturePath, filepath.Join(prepackagedPluginsDir, "testplugin2.tar.gz.sig")) + sort.Sort(pluginStatusById(pluginStatus)) + + require.Len(t, pluginStatus, 2) + expectPluginStatus(t, "testplugin", "0.0.1", pluginStatus[0]) + expectPluginStatus(t, "testplugin2", "1.2.3", pluginStatus[1]) + + expectPluginInFilestore(t, "testplugin", "0.0.1") + expectPluginNotInFilestore(t, "testplugin2") + }) + + t.Run("multiple plugins, one previously installed and now upgraded, all now installed", func(t *testing.T) { + th := setup(t) + env := th.App.GetPluginsEnvironment() + + th.App.UpdateConfig(func(cfg *model.Config) { + *cfg.PluginSettings.AutomaticPrepackagedPlugins = true + }) + + copyAsFilestorePlugin(t, "testplugin.tar.gz", "testplugin") + + th.App.UpdateConfig(func(cfg *model.Config) { + cfg.PluginSettings.PluginStates["testplugin"] = &model.PluginState{Enable: true} + cfg.PluginSettings.PluginStates["testplugin2"] = &model.PluginState{Enable: true} + }) + + copyAsPrepackagedPlugin(t, "testplugin-v0.0.2.tar.gz") + copyAsPrepackagedPlugin(t, "testplugin2.tar.gz") + + plugins, transitionalPlugins := initPlugins(t, th) + err := th.App.ch.processPrepackagedPlugins(prepackagedPluginsDir) require.NoError(t, err) - plugins := th.App.ch.processPrepackagedPlugins(prepackagedPluginsDir) - require.Len(t, plugins, 2) - require.Contains(t, []string{"testplugin", "testplugin2"}, plugins[0].Manifest.Id) - require.NotEmpty(t, plugins[0].Signature) - require.Contains(t, []string{"testplugin", "testplugin2"}, plugins[1].Manifest.Id) - require.NotEmpty(t, plugins[1].Signature) + require.Len(t, plugins, 2, "expected two prepackaged plugins") + sort.Sort(byId(plugins)) + expectPrepackagedPlugin(t, "testplugin", "0.0.2", plugins[0]) + expectPrepackagedPlugin(t, "testplugin2", "1.2.3", plugins[1]) + require.Empty(t, transitionalPlugins) + + pluginStatus, err := env.Statuses() + require.NoError(t, err) + + sort.Sort(pluginStatusById(pluginStatus)) + + require.Len(t, pluginStatus, 2) + expectPluginStatus(t, "testplugin", "0.0.2", pluginStatus[0]) + expectPluginStatus(t, "testplugin2", "1.2.3", pluginStatus[1]) + + expectPluginInFilestore(t, "testplugin", "0.0.1") + expectPluginNotInFilestore(t, "testplugin2") + }) + + t.Run("multiple plugins, one previously installed but prepackaged is older, all now installed", func(t *testing.T) { + th := setup(t) + env := th.App.GetPluginsEnvironment() + + th.App.UpdateConfig(func(cfg *model.Config) { + *cfg.PluginSettings.AutomaticPrepackagedPlugins = true + }) + + copyAsFilestorePlugin(t, "testplugin-v0.0.2.tar.gz", "testplugin") + + th.App.UpdateConfig(func(cfg *model.Config) { + cfg.PluginSettings.PluginStates["testplugin"] = &model.PluginState{Enable: true} + cfg.PluginSettings.PluginStates["testplugin2"] = &model.PluginState{Enable: true} + }) + + copyAsPrepackagedPlugin(t, "testplugin.tar.gz") + copyAsPrepackagedPlugin(t, "testplugin2.tar.gz") + + plugins, transitionalPlugins := initPlugins(t, th) + require.Len(t, plugins, 2, "expected two prepackaged plugins") + sort.Sort(byId(plugins)) + expectPrepackagedPlugin(t, "testplugin", "0.0.1", plugins[0]) + expectPrepackagedPlugin(t, "testplugin2", "1.2.3", plugins[1]) + require.Empty(t, transitionalPlugins) + + pluginStatus, err := env.Statuses() + require.NoError(t, err) + + sort.Sort(pluginStatusById(pluginStatus)) + + require.Len(t, pluginStatus, 2) + expectPluginStatus(t, "testplugin", "0.0.2", pluginStatus[0]) + expectPluginStatus(t, "testplugin2", "1.2.3", pluginStatus[1]) + + expectPluginInFilestore(t, "testplugin", "0.0.2") + expectPluginNotInFilestore(t, "testplugin2") + }) + + t.Run("multiple plugins, not automatically installed despite enabled since automatic prepackaged plugins disabled", func(t *testing.T) { + th := setup(t) + env := th.App.GetPluginsEnvironment() + + th.App.UpdateConfig(func(cfg *model.Config) { + *cfg.PluginSettings.AutomaticPrepackagedPlugins = false + cfg.PluginSettings.PluginStates["testplugin"] = &model.PluginState{Enable: true} + cfg.PluginSettings.PluginStates["testplugin2"] = &model.PluginState{Enable: true} + }) + + copyAsPrepackagedPlugin(t, "testplugin.tar.gz") + copyAsPrepackagedPlugin(t, "testplugin2.tar.gz") + + plugins, transitionalPlugins := initPlugins(t, th) + require.Len(t, plugins, 2, "expected two prepackaged plugins") + sort.Sort(byId(plugins)) + expectPrepackagedPlugin(t, "testplugin", "0.0.1", plugins[0]) + expectPrepackagedPlugin(t, "testplugin2", "1.2.3", plugins[1]) + require.Empty(t, transitionalPlugins) pluginStatus, err := env.Statuses() require.NoError(t, err) require.Len(t, pluginStatus, 0) + + expectPluginNotInFilestore(t, "testplugin") + expectPluginNotInFilestore(t, "testplugin2") + }) + + t.Run("removing a prepackaged plugin leaves it disabled", func(t *testing.T) { + th := setup(t) + env := th.App.GetPluginsEnvironment() + + th.App.UpdateConfig(func(cfg *model.Config) { + *cfg.PluginSettings.AutomaticPrepackagedPlugins = true + cfg.PluginSettings.PluginStates["testplugin"] = &model.PluginState{Enable: true} + cfg.PluginSettings.PluginStates["testplugin2"] = &model.PluginState{Enable: true} + }) + + copyAsPrepackagedPlugin(t, "testplugin.tar.gz") + copyAsPrepackagedPlugin(t, "testplugin2.tar.gz") + + plugins, transitionalPlugins := initPlugins(t, th) + require.Len(t, plugins, 2, "expected two prepackaged plugins") + sort.Sort(byId(plugins)) + expectPrepackagedPlugin(t, "testplugin", "0.0.1", plugins[0]) + expectPrepackagedPlugin(t, "testplugin2", "1.2.3", plugins[1]) + require.Empty(t, transitionalPlugins) + + pluginStatus, err := env.Statuses() + require.NoError(t, err) + + sort.Sort(pluginStatusById(pluginStatus)) + + require.Len(t, pluginStatus, 2) + require.Equal(t, "testplugin", pluginStatus[0].PluginId) + require.Equal(t, "testplugin2", pluginStatus[1].PluginId) + + appErr := th.App.ch.RemovePlugin("testplugin") + checkNoError(t, appErr) + + pluginStatus, err = env.Statuses() + require.NoError(t, err) + require.Len(t, pluginStatus, 1) + require.Equal(t, "testplugin2", pluginStatus[0].PluginId) + + plugins, transitionalPlugins = initPlugins(t, th) + require.Len(t, plugins, 2, "expected two prepackaged plugins") + sort.Sort(byId(plugins)) + expectPrepackagedPlugin(t, "testplugin", "0.0.1", plugins[0]) + expectPrepackagedPlugin(t, "testplugin2", "1.2.3", plugins[1]) + require.Empty(t, transitionalPlugins) + + pluginStatus, err = env.Statuses() + require.NoError(t, err) + require.Len(t, pluginStatus, 1) + expectPluginStatus(t, "testplugin2", "1.2.3", pluginStatus[0]) + + expectPluginNotInFilestore(t, "testplugin") + expectPluginNotInFilestore(t, "testplugin2") + }) + + t.Run("single transitional plugin automatically installed and persisted since enabled", func(t *testing.T) { + th := setup(t) + env := th.App.GetPluginsEnvironment() + + oldTransitionallyPrepackagedPlugins := append([]string{}, transitionallyPrepackagedPlugins...) + transitionallyPrepackagedPlugins = append(transitionallyPrepackagedPlugins, "testplugin") + defer func() { + transitionallyPrepackagedPlugins = oldTransitionallyPrepackagedPlugins + }() + + th.App.UpdateConfig(func(cfg *model.Config) { + *cfg.PluginSettings.AutomaticPrepackagedPlugins = true + cfg.PluginSettings.PluginStates["testplugin"] = &model.PluginState{Enable: true} + }) + + copyAsPrepackagedPlugin(t, "testplugin.tar.gz") + + plugins, transitionalPlugins := initPlugins(t, th) + require.Empty(t, plugins) + require.Len(t, transitionalPlugins, 1) + expectPrepackagedPlugin(t, "testplugin", "0.0.1", transitionalPlugins[0]) + + pluginStatus, err := env.Statuses() + require.NoError(t, err) + require.Len(t, pluginStatus, 1) + expectPluginStatus(t, "testplugin", "0.0.1", pluginStatus[0]) + + expectPluginInFilestore(t, "testplugin", "0.0.1") + expectPluginNotInFilestore(t, "testplugin2") + }) + + t.Run("single transitional plugin not persisted since already in filestore", func(t *testing.T) { + th := setup(t) + env := th.App.GetPluginsEnvironment() + + oldTransitionallyPrepackagedPlugins := append([]string{}, transitionallyPrepackagedPlugins...) + transitionallyPrepackagedPlugins = append(transitionallyPrepackagedPlugins, "testplugin") + defer func() { + transitionallyPrepackagedPlugins = oldTransitionallyPrepackagedPlugins + }() + + th.App.UpdateConfig(func(cfg *model.Config) { + *cfg.PluginSettings.AutomaticPrepackagedPlugins = true + cfg.PluginSettings.PluginStates["testplugin"] = &model.PluginState{Enable: true} + }) + + copyAsFilestorePlugin(t, "testplugin.tar.gz", "testplugin") + copyAsPrepackagedPlugin(t, "testplugin.tar.gz") + + plugins, transitionalPlugins := initPlugins(t, th) + require.Empty(t, plugins) + require.Empty(t, transitionalPlugins) + + pluginStatus, err := env.Statuses() + require.NoError(t, err) + require.Len(t, pluginStatus, 1) + expectPluginStatus(t, "testplugin", "0.0.1", pluginStatus[0]) + + expectPluginInFilestore(t, "testplugin", "0.0.1") + expectPluginNotInFilestore(t, "testplugin2") + }) + + t.Run("single transitional plugin persisted since newer than filestore", func(t *testing.T) { + th := setup(t) + env := th.App.GetPluginsEnvironment() + + oldTransitionallyPrepackagedPlugins := append([]string{}, transitionallyPrepackagedPlugins...) + transitionallyPrepackagedPlugins = append(transitionallyPrepackagedPlugins, "testplugin") + defer func() { + transitionallyPrepackagedPlugins = oldTransitionallyPrepackagedPlugins + }() + + th.App.UpdateConfig(func(cfg *model.Config) { + *cfg.PluginSettings.AutomaticPrepackagedPlugins = true + cfg.PluginSettings.PluginStates["testplugin"] = &model.PluginState{Enable: true} + }) + + copyAsFilestorePlugin(t, "testplugin.tar.gz", "testplugin") + copyAsPrepackagedPlugin(t, "testplugin-v0.0.2.tar.gz") + + plugins, transitionalPlugins := initPlugins(t, th) + require.Empty(t, plugins) + require.Len(t, transitionalPlugins, 1) + expectPrepackagedPlugin(t, "testplugin", "0.0.2", transitionalPlugins[0]) + + pluginStatus, err := env.Statuses() + require.NoError(t, err) + require.Len(t, pluginStatus, 1) + expectPluginStatus(t, "testplugin", "0.0.2", pluginStatus[0]) + + expectPluginInFilestore(t, "testplugin", "0.0.2") + expectPluginNotInFilestore(t, "testplugin2") + }) + + t.Run("transitional plugins persisted only once", func(t *testing.T) { + th := setup(t) + env := th.App.GetPluginsEnvironment() + + oldTransitionallyPrepackagedPlugins := append([]string{}, transitionallyPrepackagedPlugins...) + transitionallyPrepackagedPlugins = append(transitionallyPrepackagedPlugins, "testplugin") + defer func() { + transitionallyPrepackagedPlugins = oldTransitionallyPrepackagedPlugins + }() + + th.App.UpdateConfig(func(cfg *model.Config) { + *cfg.PluginSettings.AutomaticPrepackagedPlugins = true + cfg.PluginSettings.PluginStates["testplugin"] = &model.PluginState{Enable: true} + }) + + copyAsPrepackagedPlugin(t, "testplugin.tar.gz") + + plugins, transitionalPlugins := initPlugins(t, th) + require.Empty(t, plugins) + require.Len(t, transitionalPlugins, 1) + expectPrepackagedPlugin(t, "testplugin", "0.0.1", transitionalPlugins[0]) + + th.App.ch.RemovePlugin("testplugin") + + pluginStatus, err := env.Statuses() + require.NoError(t, err) + require.Empty(t, pluginStatus, 0) + + th.App.ch.persistTransitionallyPrepackagedPlugins() + + expectPluginNotInFilestore(t, "testplugin") + expectPluginNotInFilestore(t, "testplugin2") }) } diff --git a/server/channels/app/post.go b/server/channels/app/post.go index 48651dc8e8a..1d6cb7c65b0 100644 --- a/server/channels/app/post.go +++ b/server/channels/app/post.go @@ -19,7 +19,7 @@ import ( "github.com/mattermost/mattermost/server/public/plugin" "github.com/mattermost/mattermost/server/public/shared/i18n" "github.com/mattermost/mattermost/server/public/shared/mlog" - "github.com/mattermost/mattermost/server/v8/channels/app/request" + "github.com/mattermost/mattermost/server/public/shared/request" "github.com/mattermost/mattermost/server/v8/channels/product" "github.com/mattermost/mattermost/server/v8/channels/store" "github.com/mattermost/mattermost/server/v8/channels/store/sqlstore" @@ -98,8 +98,8 @@ func (a *App) CreatePostAsUser(c request.CTX, post *model.Post, currentSessionId // the post does NOT have from_webhook prop set (e.g. Zapier app), and // the post does NOT have from_bot set (e.g. from discovering the user is a bot within CreatePost), and // the post is NOT a reply post with CRT enabled - _, fromWebhook := post.GetProps()["from_webhook"] - _, fromBot := post.GetProps()["from_bot"] + _, fromWebhook := post.GetProps()[model.PostPropsFromWebhook] + _, fromBot := post.GetProps()[model.PostPropsFromBot] isCRTEnabled := a.IsCRTEnabledForUser(c, post.UserId) isCRTReply := post.RootId != "" && isCRTEnabled if !fromWebhook && !fromBot && !isCRTReply { @@ -236,11 +236,11 @@ func (a *App) CreatePost(c request.CTX, post *model.Post, channel *model.Channel } if user.IsBot { - post.AddProp("from_bot", "true") + post.AddProp(model.PostPropsFromBot, "true") } if c.Session().IsOAuth { - post.AddProp("from_oauth_app", "true") + post.AddProp(model.PostPropsFromOAuthApp, "true") } var ephemeralPost *model.Post @@ -356,16 +356,6 @@ func (a *App) CreatePost(c request.CTX, post *model.Post, channel *model.Channel // might be duplicating requests. a.Srv().seenPendingPostIdsCache.SetWithExpiry(post.PendingPostId, rpost.Id, PendingPostIDsCacheTTL) - // We make a copy of the post for the plugin hook to avoid a race condition, - // and to remove the non-GOB-encodable Metadata from it. - pluginPost := rpost.ForPlugin() - a.Srv().Go(func() { - a.ch.RunMultiHook(func(hooks plugin.Hooks) bool { - hooks.MessageHasBeenPosted(pluginContext, pluginPost) - return true - }, plugin.MessageHasBeenPostedID) - }) - if a.Metrics() != nil { a.Metrics().IncrementPostCreate() } @@ -380,6 +370,16 @@ func (a *App) CreatePost(c request.CTX, post *model.Post, channel *model.Channel } } + // We make a copy of the post for the plugin hook to avoid a race condition, + // and to remove the non-GOB-encodable Metadata from it. + pluginPost := rpost.ForPlugin() + a.Srv().Go(func() { + a.ch.RunMultiHook(func(hooks plugin.Hooks) bool { + hooks.MessageHasBeenPosted(pluginContext, pluginPost) + return true + }, plugin.MessageHasBeenPostedID) + }) + // Normally, we would let the API layer call PreparePostForClient, but we do it here since it also needs // to be done when we send the post over the websocket in handlePostEvents // PS: we don't want to include PostPriority from the db to avoid the replica lag, @@ -625,10 +625,10 @@ func (a *App) DeleteEphemeralPost(userID, postID string) { a.Publish(message) } -func (a *App) UpdatePost(c *request.Context, post *model.Post, safeUpdate bool) (*model.Post, *model.AppError) { - post.SanitizeProps() +func (a *App) UpdatePost(c *request.Context, receivedUpdatedPost *model.Post, safeUpdate bool) (*model.Post, *model.AppError) { + receivedUpdatedPost.SanitizeProps() - postLists, nErr := a.Srv().Store().Post().Get(context.Background(), post.Id, model.GetPostsOptions{}, "", a.Config().GetSanitizeOptions()) + postLists, nErr := a.Srv().Store().Post().Get(context.Background(), receivedUpdatedPost.Id, model.GetPostsOptions{}, "", a.Config().GetSanitizeOptions()) if nErr != nil { var nfErr *store.ErrNotFound var invErr *store.ErrInvalidInput @@ -641,21 +641,21 @@ func (a *App) UpdatePost(c *request.Context, post *model.Post, safeUpdate bool) return nil, model.NewAppError("UpdatePost", "app.post.get.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr) } } - oldPost := postLists.Posts[post.Id] + oldPost := postLists.Posts[receivedUpdatedPost.Id] var err *model.AppError if oldPost == nil { - err = model.NewAppError("UpdatePost", "api.post.update_post.find.app_error", nil, "id="+post.Id, http.StatusBadRequest) + err = model.NewAppError("UpdatePost", "api.post.update_post.find.app_error", nil, "id="+receivedUpdatedPost.Id, http.StatusBadRequest) return nil, err } if oldPost.DeleteAt != 0 { - err = model.NewAppError("UpdatePost", "api.post.update_post.permissions_details.app_error", map[string]any{"PostId": post.Id}, "", http.StatusBadRequest) + err = model.NewAppError("UpdatePost", "api.post.update_post.permissions_details.app_error", map[string]any{"PostId": receivedUpdatedPost.Id}, "", http.StatusBadRequest) return nil, err } if oldPost.IsSystemMessage() { - err = model.NewAppError("UpdatePost", "api.post.update_post.system_message.app_error", nil, "id="+post.Id, http.StatusBadRequest) + err = model.NewAppError("UpdatePost", "api.post.update_post.system_message.app_error", nil, "id="+receivedUpdatedPost.Id, http.StatusBadRequest) return nil, err } @@ -670,17 +670,17 @@ func (a *App) UpdatePost(c *request.Context, post *model.Post, safeUpdate bool) newPost := oldPost.Clone() - if newPost.Message != post.Message { - newPost.Message = post.Message + if newPost.Message != receivedUpdatedPost.Message { + newPost.Message = receivedUpdatedPost.Message newPost.EditAt = model.GetMillis() - newPost.Hashtags, _ = model.ParseHashtags(post.Message) + newPost.Hashtags, _ = model.ParseHashtags(receivedUpdatedPost.Message) } if !safeUpdate { - newPost.IsPinned = post.IsPinned - newPost.HasReactions = post.HasReactions - newPost.FileIds = post.FileIds - newPost.SetProps(post.GetProps()) + newPost.IsPinned = receivedUpdatedPost.IsPinned + newPost.HasReactions = receivedUpdatedPost.HasReactions + newPost.FileIds = receivedUpdatedPost.FileIds + newPost.SetProps(receivedUpdatedPost.GetProps()) } // Avoid deep-equal checks if EditAt was already modified through message change @@ -688,19 +688,19 @@ func (a *App) UpdatePost(c *request.Context, post *model.Post, safeUpdate bool) newPost.EditAt = model.GetMillis() } - if err = a.FillInPostProps(c, post, nil); err != nil { + if err = a.FillInPostProps(c, newPost, nil); err != nil { return nil, err } - if post.IsRemote() { - oldPost.RemoteId = model.NewString(*post.RemoteId) + if receivedUpdatedPost.IsRemote() { + oldPost.RemoteId = model.NewString(*receivedUpdatedPost.RemoteId) } var rejectionReason string pluginContext := pluginContext(c) a.ch.RunMultiHook(func(hooks plugin.Hooks) bool { newPost, rejectionReason = hooks.MessageWillBeUpdated(pluginContext, newPost.ForPlugin(), oldPost.ForPlugin()) - return post != nil + return receivedUpdatedPost != nil }, plugin.MessageWillBeUpdatedID) if newPost == nil { return nil, model.NewAppError("UpdatePost", "Post rejected by plugin. "+rejectionReason, nil, "", http.StatusBadRequest) @@ -1339,6 +1339,15 @@ func (a *App) DeletePost(c request.CTX, postID, deleteByID string) (*model.Post, a.deleteFlaggedPosts(post.Id) }) + pluginPost := post.ForPlugin() + pluginContext := pluginContext(c) + a.Srv().Go(func() { + a.ch.RunMultiHook(func(hooks plugin.Hooks) bool { + hooks.MessageHasBeenDeleted(pluginContext, pluginPost) + return true + }, plugin.MessageHasBeenDeletedID) + }) + a.Srv().Go(func() { if err = a.RemoveNotifications(c, post, channel); err != nil { a.Log().Error("DeletePost failed to delete notification", mlog.Err(err)) diff --git a/server/channels/app/post_acknowledgements.go b/server/channels/app/post_acknowledgements.go index 10fe84a7985..3af4cc17ab6 100644 --- a/server/channels/app/post_acknowledgements.go +++ b/server/channels/app/post_acknowledgements.go @@ -10,7 +10,7 @@ import ( "github.com/mattermost/mattermost/server/public/model" "github.com/mattermost/mattermost/server/public/shared/mlog" - "github.com/mattermost/mattermost/server/v8/channels/app/request" + "github.com/mattermost/mattermost/server/public/shared/request" "github.com/mattermost/mattermost/server/v8/channels/store" ) diff --git a/server/channels/app/post_metadata.go b/server/channels/app/post_metadata.go index a1aa6a93dba..d49c33e010f 100644 --- a/server/channels/app/post_metadata.go +++ b/server/channels/app/post_metadata.go @@ -22,8 +22,8 @@ import ( "github.com/mattermost/mattermost/server/public/model" "github.com/mattermost/mattermost/server/public/shared/markdown" "github.com/mattermost/mattermost/server/public/shared/mlog" + "github.com/mattermost/mattermost/server/public/shared/request" "github.com/mattermost/mattermost/server/v8/channels/app/platform" - "github.com/mattermost/mattermost/server/v8/channels/app/request" "github.com/mattermost/mattermost/server/v8/channels/utils/imgutils" ) @@ -585,6 +585,11 @@ func (a *App) containsPermalink(post *model.Post) bool { func (a *App) getLinkMetadata(c request.CTX, requestURL string, timestamp int64, isNewPost bool, previewedPostPropVal string) (*opengraph.OpenGraph, *model.PostImage, *model.Permalink, error) { requestURL = resolveMetadataURL(requestURL, a.GetSiteURL()) + // If it's an embedded image, nothing to do. + if strings.HasPrefix(strings.ToLower(requestURL), "data:image/") { + return nil, nil, nil, nil + } + timestamp = model.FloorToNearestHour(timestamp) // Check cache diff --git a/server/channels/app/post_metadata_test.go b/server/channels/app/post_metadata_test.go index e664e04b41f..9fee9cd479b 100644 --- a/server/channels/app/post_metadata_test.go +++ b/server/channels/app/post_metadata_test.go @@ -66,7 +66,6 @@ func TestPreparePostListForClient(t *testing.T) { } func TestPreparePostForClient(t *testing.T) { - t.Skip("MM-43252") var serverURL string server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch r.URL.Path { @@ -169,14 +168,13 @@ func TestPreparePostForClient(t *testing.T) { reaction1 := th.AddReactionToPost(post, th.BasicUser, "smile") reaction2 := th.AddReactionToPost(post, th.BasicUser2, "smile") reaction3 := th.AddReactionToPost(post, th.BasicUser2, "ice_cream") + reactions := []*model.Reaction{reaction1, reaction2, reaction3} post.HasReactions = true clientPost := th.App.PreparePostForClient(th.Context, post, false, false, false) assert.Len(t, clientPost.Metadata.Reactions, 3, "should've populated Reactions") - assert.Equal(t, reaction1, clientPost.Metadata.Reactions[0], "first reaction is incorrect") - assert.Equal(t, reaction2, clientPost.Metadata.Reactions[1], "second reaction is incorrect") - assert.Equal(t, reaction3, clientPost.Metadata.Reactions[2], "third reaction is incorrect") + assert.ElementsMatch(t, reactions, clientPost.Metadata.Reactions) }) t.Run("files", func(t *testing.T) { @@ -185,6 +183,7 @@ func TestPreparePostForClient(t *testing.T) { fileInfo, err := th.App.DoUploadFile(th.Context, time.Now(), th.BasicTeam.Id, th.BasicChannel.Id, th.BasicUser.Id, "test.txt", []byte("test")) fileInfo.Content = "test" + fileInfo.ChannelId = th.BasicChannel.Id require.Nil(t, err) post, err := th.App.CreatePost(th.Context, &model.Post{ @@ -2473,7 +2472,7 @@ func TestGetLinkMetadata(t *testing.T) { assert.Nil(t, img) assert.Error(t, err) assert.IsType(t, &url.Error{}, err) - assert.Equal(t, httpservice.AddressForbidden, err.(*url.Error).Err) + assert.Equal(t, httpservice.ErrAddressForbidden, err.(*url.Error).Err) requestURL = th.App.GetSiteURL() + "/api/v4/image?url=" + url.QueryEscape(requestURL) @@ -2769,52 +2768,124 @@ func TestSanitizePostMetadataForUserAndChannel(t *testing.T) { th := Setup(t).InitBasic() defer th.TearDown() + enableLinkPreviews := *th.App.Config().ServiceSettings.EnableLinkPreviews + siteURL := *th.App.Config().ServiceSettings.SiteURL + defer func() { + th.App.UpdateConfig(func(cfg *model.Config) { + cfg.ServiceSettings.EnableLinkPreviews = &enableLinkPreviews + cfg.ServiceSettings.SiteURL = &siteURL + }) + }() th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableLinkPreviews = true *cfg.ServiceSettings.SiteURL = "http://mymattermost.com" }) - directChannel, err := th.App.createDirectChannel(th.Context, th.BasicUser.Id, th.BasicUser2.Id) - assert.Nil(t, err) + t.Run("should not preview for users with no access to the channel", func(t *testing.T) { + directChannel, err := th.App.createDirectChannel(th.Context, th.BasicUser.Id, th.BasicUser2.Id) + assert.Nil(t, err) - userID := model.NewId() - post := &model.Post{ - Id: userID, - Metadata: &model.PostMetadata{ - Embeds: []*model.PostEmbed{ - { - Type: model.PostEmbedOpengraph, - URL: "ogURL", - Data: &opengraph.OpenGraph{ - Images: []*ogimage.Image{ - { - URL: "imageURL", + userID := model.NewId() + post := &model.Post{ + Id: userID, + Metadata: &model.PostMetadata{ + Embeds: []*model.PostEmbed{ + { + Type: model.PostEmbedPermalink, + Data: &model.PreviewPost{ + PostID: "permalink_post_id", + Post: &model.Post{ + Id: "permalink_post_id", + Message: "permalink post message", + ChannelId: directChannel.Id, }, }, }, }, }, - }, - } + } - previewedPost := model.NewPreviewPost(post, th.BasicTeam, directChannel) + previewedPost := model.NewPreviewPost(post, th.BasicTeam, directChannel) - actual := th.App.sanitizePostMetadataForUserAndChannel(th.Context, post, previewedPost, directChannel, th.BasicUser2.Id) - assert.NotNil(t, actual.Metadata.Embeds[0].Data) + actual := th.App.sanitizePostMetadataForUserAndChannel(th.Context, post, previewedPost, directChannel, th.BasicUser2.Id) + assert.NotNil(t, actual.Metadata.Embeds[0].Data) - guestID := model.NewId() - guest := &model.User{ - Email: "success+" + guestID + "@simulator.amazonses.com", - Username: "un_" + guestID, - Nickname: "nn_" + guestID, - Password: "Password1", - EmailVerified: true, - } - guest, appErr := th.App.CreateGuest(th.Context, guest) - require.Nil(t, appErr) + guestID := model.NewId() + guest := &model.User{ + Email: "success+" + guestID + "@simulator.amazonses.com", + Username: "un_" + guestID, + Nickname: "nn_" + guestID, + Password: "Password1", + EmailVerified: true, + } + guest, appErr := th.App.CreateGuest(th.Context, guest) + require.Nil(t, appErr) - actual = th.App.sanitizePostMetadataForUserAndChannel(th.Context, post, previewedPost, directChannel, guest.Id) - assert.Nil(t, actual.Metadata.Embeds[0].Data) + actual = th.App.sanitizePostMetadataForUserAndChannel(th.Context, post, previewedPost, directChannel, guest.Id) + assert.Nil(t, actual.Metadata.Embeds[0].Data) + }) + + t.Run("should not preview for archived channels", func(t *testing.T) { + experimentalViewArchivedChannels := *th.App.Config().TeamSettings.ExperimentalViewArchivedChannels + defer func() { + th.App.UpdateConfig(func(cfg *model.Config) { + cfg.TeamSettings.ExperimentalViewArchivedChannels = &experimentalViewArchivedChannels + }) + }() + + publicChannel, err := th.App.CreateChannel(th.Context, &model.Channel{ + Name: model.NewId(), + Type: model.ChannelTypeOpen, + TeamId: th.BasicTeam.Id, + CreatorId: th.SystemAdminUser.Id, + }, true) + + require.Nil(t, err) + require.NotEmpty(t, publicChannel.Id) + + err = th.App.DeleteChannel(th.Context, publicChannel, th.SystemAdminUser.Id) + require.Nil(t, err) + + publicChannel, err = th.App.GetChannel(th.Context, publicChannel.Id) + require.Nil(t, err) + require.NotEmpty(t, publicChannel.Id) + require.NotEqual(t, 0, publicChannel.DeleteAt) + + post := &model.Post{ + Id: th.BasicUser.Id, + Metadata: &model.PostMetadata{ + Embeds: []*model.PostEmbed{ + { + Type: model.PostEmbedPermalink, + Data: &model.PreviewPost{ + PostID: "permalink_post_id", + Post: &model.Post{ + Id: "permalink_post_id", + Message: "permalink post message", + ChannelId: publicChannel.Id, + }, + }, + }, + }, + }, + } + + previewedPost := model.NewPreviewPost(post, th.BasicTeam, publicChannel) + + th.App.UpdateConfig(func(cfg *model.Config) { + *cfg.TeamSettings.ExperimentalViewArchivedChannels = true + }) + + actual := th.App.sanitizePostMetadataForUserAndChannel(th.Context, post, previewedPost, publicChannel, th.BasicUser.Id) + assert.NotNil(t, actual.Metadata.Embeds[0].Data) + + th.App.UpdateConfig(func(cfg *model.Config) { + *cfg.TeamSettings.ExperimentalViewArchivedChannels = false + }) + + actual = th.App.sanitizePostMetadataForUserAndChannel(th.Context, post, previewedPost, publicChannel, th.BasicUser.Id) + assert.Nil(t, actual.Metadata.Embeds[0].Data) + }) } func TestSanitizePostMetaDataForAudit(t *testing.T) { @@ -2873,55 +2944,140 @@ func TestSanitizePostMetadataForUser(t *testing.T) { th := Setup(t).InitBasic() defer th.TearDown() - privateChannel, err := th.App.CreateChannel(th.Context, &model.Channel{ - Name: "private_chanenl", - Type: model.ChannelTypePrivate, - TeamId: th.BasicTeam.Id, - CreatorId: th.SystemAdminUser.Id, - }, true) + enableLinkPreviews := *th.App.Config().ServiceSettings.EnableLinkPreviews + defer func() { + th.App.UpdateConfig(func(cfg *model.Config) { + cfg.ServiceSettings.EnableLinkPreviews = &enableLinkPreviews + }) + }() + th.App.UpdateConfig(func(cfg *model.Config) { + *cfg.ServiceSettings.EnableLinkPreviews = true + }) - require.Nil(t, err) - require.NotEmpty(t, privateChannel.Id) + t.Run("should remove embeds for not accessible channels", func(t *testing.T) { + privateChannel, err := th.App.CreateChannel(th.Context, &model.Channel{ + Name: model.NewId(), + Type: model.ChannelTypePrivate, + TeamId: th.BasicTeam.Id, + CreatorId: th.SystemAdminUser.Id, + }, true) - post := &model.Post{ - Id: "post_id_1", - UserId: th.BasicUser.Id, - Metadata: &model.PostMetadata{ - Embeds: []*model.PostEmbed{ - { - Type: model.PostEmbedPermalink, - Data: &model.PreviewPost{ - PostID: "permalink_post_id", - Post: &model.Post{ - Id: "permalink_post_id", - Message: "permalink post message", - ChannelId: privateChannel.Id, + require.Nil(t, err) + require.NotEmpty(t, privateChannel.Id) + + post := &model.Post{ + Id: "post_id_1", + UserId: th.BasicUser.Id, + Metadata: &model.PostMetadata{ + Embeds: []*model.PostEmbed{ + { + Type: model.PostEmbedPermalink, + Data: &model.PreviewPost{ + PostID: "permalink_post_id", + Post: &model.Post{ + Id: "permalink_post_id", + Message: "permalink post message", + ChannelId: privateChannel.Id, + }, }, }, - }, - { - Type: model.PostEmbedPermalink, - Data: &model.PreviewPost{ - PostID: "permalink_post_id_2", - Post: &model.Post{ - Id: "permalink_post_id_2", - Message: "permalink post message 2", - ChannelId: privateChannel.Id, + { + Type: model.PostEmbedPermalink, + Data: &model.PreviewPost{ + PostID: "permalink_post_id_2", + Post: &model.Post{ + Id: "permalink_post_id_2", + Message: "permalink post message 2", + ChannelId: privateChannel.Id, + }, }, }, - }, - { - Type: model.PostEmbedLink, - URL: "https://mattermost.com", + { + Type: model.PostEmbedLink, + URL: "https://mattermost.com", + }, }, }, - }, - } + } - sanitizedPost, err := th.App.SanitizePostMetadataForUser(th.Context, post, th.BasicUser.Id) - require.Nil(t, err) - require.NotNil(t, sanitizedPost) + sanitizedPost, err := th.App.SanitizePostMetadataForUser(th.Context, post, th.BasicUser.Id) + require.Nil(t, err) + require.NotNil(t, sanitizedPost) - require.Equal(t, 1, len(sanitizedPost.Metadata.Embeds)) - require.Equal(t, model.PostEmbedLink, sanitizedPost.Metadata.Embeds[0].Type) + require.Equal(t, 1, len(sanitizedPost.Metadata.Embeds)) + require.Equal(t, model.PostEmbedLink, sanitizedPost.Metadata.Embeds[0].Type) + }) + + t.Run("should remove embeds for archived channels if the config does not allow it", func(t *testing.T) { + publicChannel, err := th.App.CreateChannel(th.Context, &model.Channel{ + Name: model.NewId(), + Type: model.ChannelTypeOpen, + TeamId: th.BasicTeam.Id, + CreatorId: th.SystemAdminUser.Id, + }, true) + + require.Nil(t, err) + require.NotEmpty(t, publicChannel.Id) + + err = th.App.DeleteChannel(th.Context, publicChannel, th.SystemAdminUser.Id) + require.Nil(t, err) + + publicChannel, err = th.App.GetChannel(th.Context, publicChannel.Id) + require.Nil(t, err) + require.NotEmpty(t, publicChannel.Id) + require.NotEqual(t, 0, publicChannel.DeleteAt) + + post := &model.Post{ + Id: "post_id_1", + UserId: th.BasicUser.Id, + Metadata: &model.PostMetadata{ + Embeds: []*model.PostEmbed{ + { + Type: model.PostEmbedPermalink, + Data: &model.PreviewPost{ + PostID: "permalink_post_id", + Post: &model.Post{ + Id: "permalink_post_id", + Message: "permalink post message", + ChannelId: publicChannel.Id, + }, + }, + }, + { + Type: model.PostEmbedLink, + URL: "https://mattermost.com", + }, + }, + }, + } + + experimentalViewArchivedChannels := *th.App.Config().TeamSettings.ExperimentalViewArchivedChannels + defer func() { + th.App.UpdateConfig(func(cfg *model.Config) { + cfg.TeamSettings.ExperimentalViewArchivedChannels = &experimentalViewArchivedChannels + }) + }() + + th.App.UpdateConfig(func(cfg *model.Config) { + *cfg.TeamSettings.ExperimentalViewArchivedChannels = true + }) + + sanitizedPost, err := th.App.SanitizePostMetadataForUser(th.Context, post, th.BasicUser.Id) + require.Nil(t, err) + require.NotNil(t, sanitizedPost) + + require.Equal(t, 2, len(sanitizedPost.Metadata.Embeds)) + require.Equal(t, model.PostEmbedPermalink, sanitizedPost.Metadata.Embeds[0].Type) + + th.App.UpdateConfig(func(cfg *model.Config) { + *cfg.TeamSettings.ExperimentalViewArchivedChannels = false + }) + + sanitizedPost, err = th.App.SanitizePostMetadataForUser(th.Context, post, th.BasicUser.Id) + require.Nil(t, err) + require.NotNil(t, sanitizedPost) + + require.Equal(t, 1, len(sanitizedPost.Metadata.Embeds)) + require.Equal(t, model.PostEmbedLink, sanitizedPost.Metadata.Embeds[0].Type) + }) } diff --git a/server/channels/app/post_persistent_notification.go b/server/channels/app/post_persistent_notification.go index 3baf5c42c4b..a8ff30f1fe4 100644 --- a/server/channels/app/post_persistent_notification.go +++ b/server/channels/app/post_persistent_notification.go @@ -10,7 +10,7 @@ import ( "github.com/mattermost/mattermost/server/public/model" "github.com/mattermost/mattermost/server/public/shared/mlog" - "github.com/mattermost/mattermost/server/v8/channels/app/request" + "github.com/mattermost/mattermost/server/public/shared/request" "github.com/mattermost/mattermost/server/v8/channels/store" "github.com/pkg/errors" ) @@ -275,9 +275,9 @@ func (a *App) channelTeamMapsForPosts(posts []*model.Post) (map[string]*model.Ch func (a *App) sendPersistentNotifications(post *model.Post, channel *model.Channel, team *model.Team, mentions *ExplicitMentions, profileMap model.UserMap, channelNotifyProps map[string]map[string]model.StringMap) error { mentionedUsersList := make(model.StringArray, 0, len(mentions.Mentions)) - for id := range mentions.Mentions { - // Don't send notification to post owner - if id != post.UserId { + for id, v := range mentions.Mentions { + // Don't send notification to post owner nor GM mentions + if id != post.UserId && v > GMMention { mentionedUsersList = append(mentionedUsersList, id) } } @@ -307,7 +307,8 @@ func (a *App) sendPersistentNotifications(post *model.Post, channel *model.Chann status = &model.Status{UserId: userID, Status: model.StatusOffline, Manual: false, LastActivityAt: 0, ActiveChannel: ""} } - if ShouldSendPushNotification(profileMap[userID], channelNotifyProps[channel.Id][userID], true, status, post) { + isGM := channel.Type == model.ChannelTypeGroup + if ShouldSendPushNotification(profileMap[userID], channelNotifyProps[channel.Id][userID], true, status, post, isGM) { a.sendPushNotification( notification, user, diff --git a/server/channels/app/post_test.go b/server/channels/app/post_test.go index 6c1110675ec..d7b46928b3d 100644 --- a/server/channels/app/post_test.go +++ b/server/channels/app/post_test.go @@ -418,7 +418,15 @@ func TestPostChannelMentions(t *testing.T) { TeamId: th.BasicTeam.Id, }, false) require.Nil(t, err) + channelToMention2, err := th.App.CreateChannel(th.Context, &model.Channel{ + DisplayName: "Mention Test2", + Name: "mention-test2", + Type: model.ChannelTypeOpen, + TeamId: th.BasicTeam.Id, + }, false) + require.Nil(t, err) defer th.App.PermanentDeleteChannel(th.Context, channelToMention) + defer th.App.PermanentDeleteChannel(th.Context, channelToMention2) _, err = th.App.AddUserToChannel(th.Context, user, channel, false) require.Nil(t, err) @@ -440,15 +448,20 @@ func TestPostChannelMentions(t *testing.T) { }, }, post.GetProp("channel_mentions")) - post.Message = fmt.Sprintf("goodbye, ~%v!", channelToMention.Name) + post.Message = fmt.Sprintf("goodbye, ~%v!", channelToMention2.Name) result, err := th.App.UpdatePost(th.Context, post, false) require.Nil(t, err) assert.Equal(t, map[string]any{ - "mention-test": map[string]any{ - "display_name": "Mention Test", + "mention-test2": map[string]any{ + "display_name": "Mention Test2", "team_name": th.BasicTeam.Name, }, }, result.GetProp("channel_mentions")) + + result.Message = "no more mentions!" + result, err = th.App.UpdatePost(th.Context, result, false) + require.Nil(t, err) + assert.Nil(t, result.GetProp("channel_mentions")) } func TestImageProxy(t *testing.T) { @@ -1113,26 +1126,6 @@ func TestCreatePostAsUser(t *testing.T) { require.Equal(t, channelMemberAfter.LastViewedAt, channelMemberBefore.LastViewedAt) }) - t.Run("logs warning for user not in channel", func(t *testing.T) { - th := Setup(t).InitBasic() - defer th.TearDown() - user := th.CreateUser() - th.LinkUserToTeam(user, th.BasicTeam) - - post := &model.Post{ - ChannelId: th.BasicChannel.Id, - Message: "test", - UserId: user.Id, - } - - _, appErr := th.App.CreatePostAsUser(th.Context, post, "", true) - require.Nil(t, appErr) - - require.NoError(t, th.TestLogger.Flush()) - - testlib.AssertLog(t, th.LogBuffer, mlog.LvlWarn.Name, "Failed to get membership") - }) - t.Run("does not log warning for bot user not in channel", func(t *testing.T) { th := Setup(t).InitBasic() defer th.TearDown() diff --git a/server/channels/app/product_notices.go b/server/channels/app/product_notices.go index 77fe812e555..95d467300e1 100644 --- a/server/channels/app/product_notices.go +++ b/server/channels/app/product_notices.go @@ -17,7 +17,7 @@ import ( "github.com/mattermost/mattermost/server/public/model" "github.com/mattermost/mattermost/server/public/shared/mlog" - "github.com/mattermost/mattermost/server/v8/channels/app/request" + "github.com/mattermost/mattermost/server/public/shared/request" "github.com/mattermost/mattermost/server/v8/channels/store" "github.com/mattermost/mattermost/server/v8/channels/utils" "github.com/mattermost/mattermost/server/v8/config" diff --git a/server/channels/app/reaction.go b/server/channels/app/reaction.go index 56e1bdc211f..9dccc3812ca 100644 --- a/server/channels/app/reaction.go +++ b/server/channels/app/reaction.go @@ -11,7 +11,7 @@ import ( "github.com/mattermost/mattermost/server/public/model" "github.com/mattermost/mattermost/server/public/plugin" "github.com/mattermost/mattermost/server/public/shared/mlog" - "github.com/mattermost/mattermost/server/v8/channels/app/request" + "github.com/mattermost/mattermost/server/public/shared/request" ) func (a *App) SaveReactionForPost(c *request.Context, reaction *model.Reaction) (*model.Reaction, *model.AppError) { diff --git a/server/channels/app/role.go b/server/channels/app/role.go index 15c95b15955..82a87fbc730 100644 --- a/server/channels/app/role.go +++ b/server/channels/app/role.go @@ -12,6 +12,8 @@ import ( "strings" "github.com/mattermost/mattermost/server/public/model" + pUtils "github.com/mattermost/mattermost/server/public/utils" + "github.com/mattermost/mattermost/server/v8/channels/store" "github.com/mattermost/mattermost/server/v8/channels/utils" ) @@ -187,13 +189,13 @@ func (a *App) UpdateRole(role *model.Role) (*model.Role, *model.AppError) { builtInRolesMinusChannelRoles := append(utils.RemoveStringsFromSlice(model.BuiltInSchemeManagedRoleIDs, builtInChannelRoles...), model.NewSystemRoleIDs...) - if utils.StringInSlice(savedRole.Name, builtInRolesMinusChannelRoles) { + if pUtils.Contains(builtInRolesMinusChannelRoles, savedRole.Name) { return savedRole, nil } var roleRetrievalFunc func() ([]*model.Role, *model.AppError) - if utils.StringInSlice(savedRole.Name, builtInChannelRoles) { + if pUtils.Contains(builtInChannelRoles, savedRole.Name) { roleRetrievalFunc = func() ([]*model.Role, *model.AppError) { roles, nErr := a.Srv().Store().Role().AllChannelSchemeRoles() if nErr != nil { diff --git a/server/channels/app/role_test.go b/server/channels/app/role_test.go index 04f9f9bd88d..100de83a4fc 100644 --- a/server/channels/app/role_test.go +++ b/server/channels/app/role_test.go @@ -15,7 +15,7 @@ import ( "github.com/stretchr/testify/require" "github.com/mattermost/mattermost/server/public/model" - "github.com/mattermost/mattermost/server/v8/channels/utils" + pUtils "github.com/mattermost/mattermost/server/public/utils" ) type permissionInheritanceTestData struct { @@ -37,7 +37,7 @@ func TestGetRolesByNames(t *testing.T) { require.NotNil(t, actualRole) require.Equal(t, testData.channelRole.Name, actualRole.Name) - require.Equal(t, testData.shouldHavePermission, utils.StringInSlice(testData.permission.Id, actualRole.Permissions)) + require.Equal(t, testData.shouldHavePermission, pUtils.Contains(actualRole.Permissions, testData.permission.Id)) }) } @@ -47,7 +47,7 @@ func TestGetRoleByName(t *testing.T) { require.Nil(t, err) require.NotNil(t, actualRole) require.Equal(t, testData.channelRole.Name, actualRole.Name) - require.Equal(t, testData.shouldHavePermission, utils.StringInSlice(testData.permission.Id, actualRole.Permissions), "row: %+v", testData.truthTableRow) + require.Equal(t, testData.shouldHavePermission, pUtils.Contains(actualRole.Permissions, testData.permission.Id), "row: %+v", testData.truthTableRow) }) } @@ -57,7 +57,7 @@ func TestGetRoleByID(t *testing.T) { require.Nil(t, err) require.NotNil(t, actualRole) require.Equal(t, testData.channelRole.Id, actualRole.Id) - require.Equal(t, testData.shouldHavePermission, utils.StringInSlice(testData.permission.Id, actualRole.Permissions), "row: %+v", testData.truthTableRow) + require.Equal(t, testData.shouldHavePermission, pUtils.Contains(actualRole.Permissions, testData.permission.Id), "row: %+v", testData.truthTableRow) }) } @@ -69,7 +69,7 @@ func TestGetAllRoles(t *testing.T) { if actualRole.Id == testData.channelRole.Id { require.NotNil(t, actualRole) require.Equal(t, testData.channelRole.Id, actualRole.Id) - require.Equal(t, testData.shouldHavePermission, utils.StringInSlice(testData.permission.Id, actualRole.Permissions), "row: %+v", testData.truthTableRow) + require.Equal(t, testData.shouldHavePermission, pUtils.Contains(actualRole.Permissions, testData.permission.Id), "row: %+v", testData.truthTableRow) } } }) diff --git a/server/channels/app/server.go b/server/channels/app/server.go index a45d7cc8b7e..a93b79bbcc0 100644 --- a/server/channels/app/server.go +++ b/server/channels/app/server.go @@ -30,15 +30,16 @@ import ( "github.com/mattermost/mattermost/server/public/model" "github.com/mattermost/mattermost/server/public/shared/i18n" "github.com/mattermost/mattermost/server/public/shared/mlog" + "github.com/mattermost/mattermost/server/public/shared/request" "github.com/mattermost/mattermost/server/public/shared/timezones" "github.com/mattermost/mattermost/server/v8/channels/app/email" "github.com/mattermost/mattermost/server/v8/channels/app/platform" - "github.com/mattermost/mattermost/server/v8/channels/app/request" "github.com/mattermost/mattermost/server/v8/channels/app/teams" "github.com/mattermost/mattermost/server/v8/channels/app/users" "github.com/mattermost/mattermost/server/v8/channels/audit" "github.com/mattermost/mattermost/server/v8/channels/jobs" "github.com/mattermost/mattermost/server/v8/channels/jobs/active_users" + "github.com/mattermost/mattermost/server/v8/channels/jobs/cleanup_desktop_tokens" "github.com/mattermost/mattermost/server/v8/channels/jobs/expirynotify" "github.com/mattermost/mattermost/server/v8/channels/jobs/export_delete" "github.com/mattermost/mattermost/server/v8/channels/jobs/export_process" @@ -1495,7 +1496,7 @@ func (ch *Channels) ClientConfigHash() string { } func (s *Server) initJobs() { - s.Jobs = jobs.NewJobServer(s.platform, s.Store(), s.GetMetrics()) + s.Jobs = jobs.NewJobServer(s.platform, s.Store(), s.GetMetrics(), s.Log()) if jobsDataRetentionJobInterface != nil { builder := jobsDataRetentionJobInterface(s) @@ -1646,6 +1647,12 @@ func (s *Server) initJobs() { hosted_purchase_screening.MakeScheduler(s.Jobs, s.License()), ) + s.Jobs.RegisterJobType( + model.JobTypeCleanupDesktopTokens, + cleanup_desktop_tokens.MakeWorker(s.Jobs, s.Store()), + cleanup_desktop_tokens.MakeScheduler(s.Jobs), + ) + s.platform.Jobs = s.Jobs } diff --git a/server/channels/app/shared_channel.go b/server/channels/app/shared_channel.go index b4d0043589e..47d0158140e 100644 --- a/server/channels/app/shared_channel.go +++ b/server/channels/app/shared_channel.go @@ -9,7 +9,7 @@ import ( "net/http" "github.com/mattermost/mattermost/server/public/model" - "github.com/mattermost/mattermost/server/v8/channels/app/request" + "github.com/mattermost/mattermost/server/public/shared/request" "github.com/mattermost/mattermost/server/v8/channels/store" ) diff --git a/server/channels/app/slack.go b/server/channels/app/slack.go index b4c5a0d079e..1d832fdd42a 100644 --- a/server/channels/app/slack.go +++ b/server/channels/app/slack.go @@ -14,7 +14,7 @@ import ( "time" "github.com/mattermost/mattermost/server/public/model" - "github.com/mattermost/mattermost/server/v8/channels/app/request" + "github.com/mattermost/mattermost/server/public/shared/request" "github.com/mattermost/mattermost/server/v8/channels/store" "github.com/mattermost/mattermost/server/v8/platform/services/slackimport" ) diff --git a/server/channels/app/slashcommands/auto_channels.go b/server/channels/app/slashcommands/auto_channels.go index 0e56ab99886..1881766882e 100644 --- a/server/channels/app/slashcommands/auto_channels.go +++ b/server/channels/app/slashcommands/auto_channels.go @@ -5,8 +5,8 @@ package slashcommands import ( "github.com/mattermost/mattermost/server/public/model" + "github.com/mattermost/mattermost/server/public/shared/request" "github.com/mattermost/mattermost/server/v8/channels/app" - "github.com/mattermost/mattermost/server/v8/channels/app/request" "github.com/mattermost/mattermost/server/v8/channels/utils" ) diff --git a/server/channels/app/slashcommands/auto_environment.go b/server/channels/app/slashcommands/auto_environment.go index 9e73a20dbe0..664537e0f4c 100644 --- a/server/channels/app/slashcommands/auto_environment.go +++ b/server/channels/app/slashcommands/auto_environment.go @@ -9,8 +9,8 @@ import ( "time" "github.com/mattermost/mattermost/server/public/model" + "github.com/mattermost/mattermost/server/public/shared/request" "github.com/mattermost/mattermost/server/v8/channels/app" - "github.com/mattermost/mattermost/server/v8/channels/app/request" "github.com/mattermost/mattermost/server/v8/channels/utils" ) diff --git a/server/channels/app/slashcommands/auto_posts.go b/server/channels/app/slashcommands/auto_posts.go index 573c9c0120c..7cea8bc7de1 100644 --- a/server/channels/app/slashcommands/auto_posts.go +++ b/server/channels/app/slashcommands/auto_posts.go @@ -10,8 +10,8 @@ import ( "path/filepath" "github.com/mattermost/mattermost/server/public/model" + "github.com/mattermost/mattermost/server/public/shared/request" "github.com/mattermost/mattermost/server/v8/channels/app" - "github.com/mattermost/mattermost/server/v8/channels/app/request" "github.com/mattermost/mattermost/server/v8/channels/utils" "github.com/mattermost/mattermost/server/v8/channels/utils/fileutils" ) diff --git a/server/channels/app/slashcommands/auto_users.go b/server/channels/app/slashcommands/auto_users.go index d2aa7e765ad..32c0afc41b2 100644 --- a/server/channels/app/slashcommands/auto_users.go +++ b/server/channels/app/slashcommands/auto_users.go @@ -9,8 +9,8 @@ import ( "net/http" "github.com/mattermost/mattermost/server/public/model" + "github.com/mattermost/mattermost/server/public/shared/request" "github.com/mattermost/mattermost/server/v8/channels/app" - "github.com/mattermost/mattermost/server/v8/channels/app/request" "github.com/mattermost/mattermost/server/v8/channels/store" "github.com/mattermost/mattermost/server/v8/channels/utils" ) diff --git a/server/channels/app/slashcommands/command_away.go b/server/channels/app/slashcommands/command_away.go index 89fa9e26076..28c23689ac6 100644 --- a/server/channels/app/slashcommands/command_away.go +++ b/server/channels/app/slashcommands/command_away.go @@ -6,8 +6,8 @@ package slashcommands import ( "github.com/mattermost/mattermost/server/public/model" "github.com/mattermost/mattermost/server/public/shared/i18n" + "github.com/mattermost/mattermost/server/public/shared/request" "github.com/mattermost/mattermost/server/v8/channels/app" - "github.com/mattermost/mattermost/server/v8/channels/app/request" ) type AwayProvider struct { diff --git a/server/channels/app/slashcommands/command_channel_header.go b/server/channels/app/slashcommands/command_channel_header.go index 7996ac48fd8..0403e14274d 100644 --- a/server/channels/app/slashcommands/command_channel_header.go +++ b/server/channels/app/slashcommands/command_channel_header.go @@ -6,8 +6,8 @@ package slashcommands import ( "github.com/mattermost/mattermost/server/public/model" "github.com/mattermost/mattermost/server/public/shared/i18n" + "github.com/mattermost/mattermost/server/public/shared/request" "github.com/mattermost/mattermost/server/v8/channels/app" - "github.com/mattermost/mattermost/server/v8/channels/app/request" ) type HeaderProvider struct { diff --git a/server/channels/app/slashcommands/command_channel_purpose.go b/server/channels/app/slashcommands/command_channel_purpose.go index 0cdf813637e..62fc4e84175 100644 --- a/server/channels/app/slashcommands/command_channel_purpose.go +++ b/server/channels/app/slashcommands/command_channel_purpose.go @@ -6,8 +6,8 @@ package slashcommands import ( "github.com/mattermost/mattermost/server/public/model" "github.com/mattermost/mattermost/server/public/shared/i18n" + "github.com/mattermost/mattermost/server/public/shared/request" "github.com/mattermost/mattermost/server/v8/channels/app" - "github.com/mattermost/mattermost/server/v8/channels/app/request" ) type PurposeProvider struct { diff --git a/server/channels/app/slashcommands/command_channel_rename.go b/server/channels/app/slashcommands/command_channel_rename.go index a84d76053a1..c387ee2265a 100644 --- a/server/channels/app/slashcommands/command_channel_rename.go +++ b/server/channels/app/slashcommands/command_channel_rename.go @@ -6,8 +6,8 @@ package slashcommands import ( "github.com/mattermost/mattermost/server/public/model" "github.com/mattermost/mattermost/server/public/shared/i18n" + "github.com/mattermost/mattermost/server/public/shared/request" "github.com/mattermost/mattermost/server/v8/channels/app" - "github.com/mattermost/mattermost/server/v8/channels/app/request" ) type RenameProvider struct { diff --git a/server/channels/app/slashcommands/command_code.go b/server/channels/app/slashcommands/command_code.go index c8582cd5f79..6b7f9acecf1 100644 --- a/server/channels/app/slashcommands/command_code.go +++ b/server/channels/app/slashcommands/command_code.go @@ -8,8 +8,8 @@ import ( "github.com/mattermost/mattermost/server/public/model" "github.com/mattermost/mattermost/server/public/shared/i18n" + "github.com/mattermost/mattermost/server/public/shared/request" "github.com/mattermost/mattermost/server/v8/channels/app" - "github.com/mattermost/mattermost/server/v8/channels/app/request" ) type CodeProvider struct { diff --git a/server/channels/app/slashcommands/command_custom_status.go b/server/channels/app/slashcommands/command_custom_status.go index 0851fb3b4e2..809e707e252 100644 --- a/server/channels/app/slashcommands/command_custom_status.go +++ b/server/channels/app/slashcommands/command_custom_status.go @@ -11,8 +11,8 @@ import ( "github.com/mattermost/mattermost/server/public/model" "github.com/mattermost/mattermost/server/public/shared/i18n" "github.com/mattermost/mattermost/server/public/shared/mlog" + "github.com/mattermost/mattermost/server/public/shared/request" "github.com/mattermost/mattermost/server/v8/channels/app" - "github.com/mattermost/mattermost/server/v8/channels/app/request" ) type CustomStatusProvider struct { diff --git a/server/channels/app/slashcommands/command_dnd.go b/server/channels/app/slashcommands/command_dnd.go index ac34f47cb4a..1f613d910fd 100644 --- a/server/channels/app/slashcommands/command_dnd.go +++ b/server/channels/app/slashcommands/command_dnd.go @@ -6,8 +6,8 @@ package slashcommands import ( "github.com/mattermost/mattermost/server/public/model" "github.com/mattermost/mattermost/server/public/shared/i18n" + "github.com/mattermost/mattermost/server/public/shared/request" "github.com/mattermost/mattermost/server/v8/channels/app" - "github.com/mattermost/mattermost/server/v8/channels/app/request" ) type DndProvider struct { diff --git a/server/channels/app/slashcommands/command_echo.go b/server/channels/app/slashcommands/command_echo.go index f2dd04e2c0c..493d18f4305 100644 --- a/server/channels/app/slashcommands/command_echo.go +++ b/server/channels/app/slashcommands/command_echo.go @@ -11,8 +11,8 @@ import ( "github.com/mattermost/mattermost/server/public/model" "github.com/mattermost/mattermost/server/public/shared/i18n" "github.com/mattermost/mattermost/server/public/shared/mlog" + "github.com/mattermost/mattermost/server/public/shared/request" "github.com/mattermost/mattermost/server/v8/channels/app" - "github.com/mattermost/mattermost/server/v8/channels/app/request" ) var echoSem chan bool diff --git a/server/channels/app/slashcommands/command_expand_collapse.go b/server/channels/app/slashcommands/command_expand_collapse.go index 95d40f49035..4ed28fde0c5 100644 --- a/server/channels/app/slashcommands/command_expand_collapse.go +++ b/server/channels/app/slashcommands/command_expand_collapse.go @@ -9,8 +9,8 @@ import ( "github.com/mattermost/mattermost/server/public/model" "github.com/mattermost/mattermost/server/public/shared/i18n" + "github.com/mattermost/mattermost/server/public/shared/request" "github.com/mattermost/mattermost/server/v8/channels/app" - "github.com/mattermost/mattermost/server/v8/channels/app/request" ) type ExpandProvider struct { diff --git a/server/channels/app/slashcommands/command_exportlink.go b/server/channels/app/slashcommands/command_exportlink.go index e0780a0051f..81a75a58caa 100644 --- a/server/channels/app/slashcommands/command_exportlink.go +++ b/server/channels/app/slashcommands/command_exportlink.go @@ -11,8 +11,8 @@ import ( "github.com/mattermost/logr/v2" "github.com/mattermost/mattermost/server/public/model" "github.com/mattermost/mattermost/server/public/shared/i18n" + "github.com/mattermost/mattermost/server/public/shared/request" "github.com/mattermost/mattermost/server/v8/channels/app" - "github.com/mattermost/mattermost/server/v8/channels/app/request" "github.com/mattermost/mattermost/server/v8/platform/shared/filestore" ) diff --git a/server/channels/app/slashcommands/command_groupmsg.go b/server/channels/app/slashcommands/command_groupmsg.go index ea820fe6c8b..43eb1868763 100644 --- a/server/channels/app/slashcommands/command_groupmsg.go +++ b/server/channels/app/slashcommands/command_groupmsg.go @@ -10,8 +10,8 @@ import ( "github.com/mattermost/mattermost/server/public/model" "github.com/mattermost/mattermost/server/public/shared/i18n" "github.com/mattermost/mattermost/server/public/shared/mlog" + "github.com/mattermost/mattermost/server/public/shared/request" "github.com/mattermost/mattermost/server/v8/channels/app" - "github.com/mattermost/mattermost/server/v8/channels/app/request" ) type groupmsgProvider struct { diff --git a/server/channels/app/slashcommands/command_help.go b/server/channels/app/slashcommands/command_help.go index 0d1509e3781..87296867dbf 100644 --- a/server/channels/app/slashcommands/command_help.go +++ b/server/channels/app/slashcommands/command_help.go @@ -6,8 +6,8 @@ package slashcommands import ( "github.com/mattermost/mattermost/server/public/model" "github.com/mattermost/mattermost/server/public/shared/i18n" + "github.com/mattermost/mattermost/server/public/shared/request" "github.com/mattermost/mattermost/server/v8/channels/app" - "github.com/mattermost/mattermost/server/v8/channels/app/request" ) type HelpProvider struct { diff --git a/server/channels/app/slashcommands/command_invite.go b/server/channels/app/slashcommands/command_invite.go index b2d04bcb212..e3ea1d82fa4 100644 --- a/server/channels/app/slashcommands/command_invite.go +++ b/server/channels/app/slashcommands/command_invite.go @@ -8,8 +8,9 @@ import ( "github.com/mattermost/mattermost/server/public/model" "github.com/mattermost/mattermost/server/public/shared/i18n" + "github.com/mattermost/mattermost/server/public/shared/mlog" + "github.com/mattermost/mattermost/server/public/shared/request" "github.com/mattermost/mattermost/server/v8/channels/app" - "github.com/mattermost/mattermost/server/v8/channels/app/request" ) type InviteProvider struct { @@ -19,6 +20,16 @@ const ( CmdInvite = "invite" ) +type UserError int64 + +const ( + NoError UserError = iota + UserInChannel + UserNotInTeam + IsConstrained + Unknown +) + func init() { app.RegisterCommandProvider(&InviteProvider{}) } @@ -59,28 +70,140 @@ func (i *InviteProvider) doCommand(a *app.App, c request.CTX, args *model.Comman // Verify that the inviter has permissions to invite users to the every channel. targetChannels = i.checkPermissions(a, c, args, resps, targetUsers[0], targetChannels) - for _, targetUser := range targetUsers { - for _, targetChannel := range targetChannels { - if resp = i.addUserToChannel(a, c, args, targetUser, targetChannel); resp != "" { - *resps = append(*resps, resp) - continue - } - if args.ChannelId != targetChannel.Id { - *resps = append(*resps, args.T("api.command_invite.success", map[string]any{ - "User": targetUser.Username, - "Channel": targetChannel.Name, - })) + // track errors returned for various users. + differentChannels := make(map[string][]string) + nonTeamUsers := make(map[string][]string) + channelConstrained := make([]string, 0, 1) + usersInChannel := make([]string, 0, 1) + errorUsers := make([]string, 0, 1) + + for _, targetChannel := range targetChannels { + var targetTeamDisplay string + for _, targetUser := range targetUsers { + userError := i.addUserToChannel(a, c, args, targetUser, targetChannel) + if userError == NoError { + if args.ChannelId != targetChannel.Id { + differentChannels[targetChannel.Name] = append(differentChannels[targetChannel.Name], targetUser.Username) + } + } else if userError == UserNotInTeam { + if targetTeamDisplay == "" { + targetTeam, err := a.GetTeam(targetChannel.TeamId) + if err != nil { + targetTeamDisplay = "unknown" + } else { + targetTeamDisplay = targetTeam.DisplayName + } + } + nonTeamUsers[targetTeamDisplay] = append(nonTeamUsers[targetTeamDisplay], targetUser.Username) + } else if userError == IsConstrained { + channelConstrained = append(channelConstrained, targetUser.Username) + } else if userError == UserInChannel { + usersInChannel = append(usersInChannel, targetUser.Username) + } else { + errorUsers = append(errorUsers, targetUser.Username) } } } + if len(usersInChannel) > 0 { + if len(usersInChannel) > 10 { + *resps = append(*resps, + args.T("api.command_invite.user_already_in_channel.overflow", map[string]any{ + "FirstUser": "@" + usersInChannel[0], + "Others": len(usersInChannel) - 1, + }), + ) + } else { + usersString := map[string]any{ + "User": "@" + strings.Join(usersInChannel, ", @"), + } + *resps = append(*resps, + args.T("api.command_invite.user_already_in_channel.app_error", len(usersInChannel), usersString), + ) + } + } + + if len(differentChannels) > 0 { + for k, v := range differentChannels { + if len(v) > 10 { + *resps = append(*resps, + args.T("api.command_invite.successOverflow", map[string]any{ + "FirstUser": "@" + v[0], + "Others": len(v) - 1, + "Channel": k, + }), + ) + } else { + usersString := map[string]any{ + "User": "@" + strings.Join(v, ", @"), + "Channel": k, + } + *resps = append(*resps, + args.T("api.command_invite.success", usersString), + ) + } + } + } + + if len(nonTeamUsers) > 0 { + for k, v := range nonTeamUsers { + if len(v) > 10 { + *resps = append(*resps, + args.T("api.command_invite.user_not_in_team.messageOverflow", map[string]any{ + "FirstUser": "@" + v[0], + "Others": len(v) - 1, + "Team": k, + }), + ) + } else { + usersString := map[string]any{ + "Users": "@" + strings.Join(v, ", @"), + "Team": k, + } + *resps = append(*resps, + args.T("api.command_invite.user_not_in_team.app_error", usersString), + ) + } + } + } + + if len(channelConstrained) > 0 { + *resps = append(*resps, + args.T("api.command_invite.channel_constrained_user_denied"), + ) + } + + if len(errorUsers) > 0 { + *resps = append(*resps, + args.T("api.command_invite.fail.app_error"), + ) + } + if len(*resps) > 0 { return strings.Join(*resps, "\n") } - return "" } +func (i *InviteProvider) getUsersFromMentionName(a *app.App, mentionName string) []*model.User { + userProfile, err := a.Srv().Store().User().GetByUsername(mentionName) + if err == nil && userProfile.DeleteAt == 0 { + return []*model.User{userProfile} + } + + group, appErr := a.GetGroupByName(mentionName, model.GroupSearchOpts{FilterAllowReference: true}) + if appErr != nil || group == nil { + return nil + } + + members, appErr := a.GetGroupMemberUsers(group.Id) + if appErr != nil { + return nil + } + + return members +} + func (i *InviteProvider) parseMessage(a *app.App, c request.CTX, args *model.CommandArgs, resps *[]string, message string) ([]*model.User, []*model.Channel, string) { splitMessage := strings.Split(message, " ") @@ -93,15 +216,15 @@ func (i *InviteProvider) parseMessage(a *app.App, c request.CTX, args *model.Com } if msg[0] == '@' || (msg[0] != '~' && j == 0) { - targetUsername := strings.TrimPrefix(msg, "@") - userProfile := i.getUserProfile(a, targetUsername) - if userProfile == nil { + targetMentionName := strings.TrimPrefix(msg, "@") + users := i.getUsersFromMentionName(a, targetMentionName) + if len(users) == 0 { *resps = append(*resps, args.T("api.command_invite.missing_user.app_error", map[string]any{ - "User": targetUsername, + "User": targetMentionName, })) continue } - targetUsers = append(targetUsers, userProfile) + targetUsers = append(targetUsers, users...) } else { targetChannelName := strings.TrimPrefix(msg, "~") channelToJoin, err := a.GetChannelByName(c, targetChannelName, args.TeamId, false) @@ -137,19 +260,6 @@ func (i *InviteProvider) parseMessage(a *app.App, c request.CTX, args *model.Com return targetUsers, targetChannels, "" } -func (i *InviteProvider) getUserProfile(a *app.App, username string) *model.User { - userProfile, nErr := a.Srv().Store().User().GetByUsername(username) - if nErr != nil { - return nil - } - - if userProfile.DeleteAt != 0 { - return nil - } - - return userProfile -} - func (i *InviteProvider) checkPermissions(a *app.App, c request.CTX, args *model.CommandArgs, resps *[]string, targetUser *model.User, targetChannels []*model.Channel) []*model.Channel { var err *model.AppError validChannels := make([]*model.Channel, 0, len(targetChannels)) @@ -188,26 +298,23 @@ func (i *InviteProvider) checkPermissions(a *app.App, c request.CTX, args *model return validChannels } -func (i *InviteProvider) addUserToChannel(a *app.App, c request.CTX, args *model.CommandArgs, userProfile *model.User, channelToJoin *model.Channel) string { +func (i *InviteProvider) addUserToChannel(a *app.App, c request.CTX, args *model.CommandArgs, userProfile *model.User, channelToJoin *model.Channel) UserError { // Check if user is already in the channel _, err := a.GetChannelMember(c, channelToJoin.Id, userProfile.Id) if err == nil { - return args.T("api.command_invite.user_already_in_channel.app_error", map[string]any{ - "User": userProfile.Username, - }) + return UserInChannel } if _, err = a.AddChannelMember(c, userProfile.Id, channelToJoin, app.ChannelMemberOpts{UserRequestorID: args.UserId}); err != nil { if err.Id == "api.channel.add_members.user_denied" { - return args.T("api.command_invite.group_constrained_user_denied") + return IsConstrained } else if err.Id == "app.team.get_member.missing.app_error" || err.Id == "api.channel.add_user.to.channel.failed.deleted.app_error" { - return args.T("api.command_invite.user_not_in_team.app_error", map[string]any{ - "Username": userProfile.Username, - }) + return UserNotInTeam } - return args.T("api.command_invite.fail.app_error") + mlog.Warn("addUserToChannel had unexpected error.", mlog.String("UserId", userProfile.Id), mlog.Err(err)) + return Unknown } - return "" + return NoError } diff --git a/server/channels/app/slashcommands/command_invite_people.go b/server/channels/app/slashcommands/command_invite_people.go index 2bfede51f52..702e13a48b4 100644 --- a/server/channels/app/slashcommands/command_invite_people.go +++ b/server/channels/app/slashcommands/command_invite_people.go @@ -9,8 +9,8 @@ import ( "github.com/mattermost/mattermost/server/public/model" "github.com/mattermost/mattermost/server/public/shared/i18n" "github.com/mattermost/mattermost/server/public/shared/mlog" + "github.com/mattermost/mattermost/server/public/shared/request" "github.com/mattermost/mattermost/server/v8/channels/app" - "github.com/mattermost/mattermost/server/v8/channels/app/request" ) type InvitePeopleProvider struct { diff --git a/server/channels/app/slashcommands/command_invite_test.go b/server/channels/app/slashcommands/command_invite_test.go index 9a4aeffbe4d..5923ceb8ad7 100644 --- a/server/channels/app/slashcommands/command_invite_test.go +++ b/server/channels/app/slashcommands/command_invite_test.go @@ -74,7 +74,7 @@ func TestInviteProvider(t *testing.T) { channel2 := th.createChannel(th.BasicTeam, model.ChannelTypeOpen) msg := "@" + th.BasicUser2.Username + " @" + anotherUser.Username + " ~" + channel1.Name + " ~" + channel2.Name - expected := "api.command_invite.success\napi.command_invite.success\napi.command_invite.success\napi.command_invite.success" + expected := "api.command_invite.success\napi.command_invite.success" runCmd(msg, expected) checkIsMember(channel1.Id, th.BasicUser2.Id) checkIsMember(channel2.Id, th.BasicUser2.Id) @@ -149,7 +149,7 @@ func TestInviteProvider(t *testing.T) { groupChannel, _ = th.App.UpdateChannel(th.Context, groupChannel) msg := "@" + th.BasicUser2.Username + " ~" + groupChannel.Name - runCmd(msg, "api.command_invite.group_constrained_user_denied") + runCmd(msg, "api.command_invite.channel_constrained_user_denied") checkIsNotMember(groupChannel.Id, th.BasicUser2.Id) }) @@ -264,3 +264,81 @@ func TestInviteGroup(t *testing.T) { }) } } + +func TestUserGroups(t *testing.T) { + th := setup(t).initBasic() + defer th.tearDown() + + privateChannel := th.createChannel(th.BasicTeam, model.ChannelTypePrivate) + + id := model.NewId() + teamGroup, err := th.App.CreateGroup(&model.Group{ + DisplayName: "dn_" + id, + Name: model.NewString("name" + id), + Source: model.GroupSourceCustom, + Description: "description_" + id, + AllowReference: true, + // MemberIDs: []string{th.BasicUser2.Id}, + }) + assert.Nil(t, err) + teamGroupCommand := "@" + *teamGroup.Name + " ~" + privateChannel.Name + + // th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuProfessional)) + groupMembers, upsertErr := th.App.UpsertGroupMembers(teamGroup.Id, []string{th.BasicUser2.Id}) + require.Nil(t, upsertErr) + assert.Len(t, groupMembers, 1) + + basicUser3 := th.createUser() + basicUser4 := th.createUser() + id2 := model.NewId() + nonTeamGroup, err := th.App.CreateGroup(&model.Group{ + DisplayName: "dn_" + id2, + Name: model.NewString("name" + id2), + Source: model.GroupSourceCustom, + Description: "description_" + id2, + AllowReference: true, + // MemberIDs: []string{basicUser3.Id, basicUser4.Id}, + }) + assert.Nil(t, err) + nonTeamGroupCommand := "@" + *nonTeamGroup.Name + " ~" + privateChannel.Name + nonTeamGroupMembers, upsertErr := th.App.UpsertGroupMembers(nonTeamGroup.Id, []string{basicUser3.Id, basicUser4.Id}) + require.Nil(t, upsertErr) + assert.Len(t, nonTeamGroupMembers, 2) + + InviteP := InviteProvider{} + args := &model.CommandArgs{ + T: func(s string, args ...any) string { return s }, + ChannelId: th.BasicChannel.Id, + TeamId: th.BasicTeam.Id, + UserId: th.BasicUser.Id, + } + + tests := []struct { + desc string + expected string + msg string + }{ + { + desc: "try to add an new group of users ", + expected: "api.command_invite.success", + msg: teamGroupCommand, + }, + { + desc: "try to add existing users", + expected: "api.command_invite.user_already_in_channel.app_error", + msg: teamGroupCommand, + }, + { + desc: "try to add a user NOT part of the team", + expected: "api.command_invite.user_not_in_team.app_error", + msg: nonTeamGroupCommand, + }, + } + + for _, test := range tests { + t.Run(test.desc, func(t *testing.T) { + actual := InviteP.DoCommand(th.App, th.Context, args, test.msg).Text + assert.Equal(t, test.expected, actual) + }) + } +} diff --git a/server/channels/app/slashcommands/command_join.go b/server/channels/app/slashcommands/command_join.go index 9e894aadd78..2bc6be9066f 100644 --- a/server/channels/app/slashcommands/command_join.go +++ b/server/channels/app/slashcommands/command_join.go @@ -8,8 +8,8 @@ import ( "github.com/mattermost/mattermost/server/public/model" "github.com/mattermost/mattermost/server/public/shared/i18n" + "github.com/mattermost/mattermost/server/public/shared/request" "github.com/mattermost/mattermost/server/v8/channels/app" - "github.com/mattermost/mattermost/server/v8/channels/app/request" ) type JoinProvider struct { diff --git a/server/channels/app/slashcommands/command_leave.go b/server/channels/app/slashcommands/command_leave.go index 33dc1cbc2b0..81a1898e500 100644 --- a/server/channels/app/slashcommands/command_leave.go +++ b/server/channels/app/slashcommands/command_leave.go @@ -6,8 +6,8 @@ package slashcommands import ( "github.com/mattermost/mattermost/server/public/model" "github.com/mattermost/mattermost/server/public/shared/i18n" + "github.com/mattermost/mattermost/server/public/shared/request" "github.com/mattermost/mattermost/server/v8/channels/app" - "github.com/mattermost/mattermost/server/v8/channels/app/request" ) type LeaveProvider struct { diff --git a/server/channels/app/slashcommands/command_loadtest.go b/server/channels/app/slashcommands/command_loadtest.go index 18dc3ada290..632bacf7dd2 100644 --- a/server/channels/app/slashcommands/command_loadtest.go +++ b/server/channels/app/slashcommands/command_loadtest.go @@ -18,8 +18,8 @@ import ( "github.com/mattermost/mattermost/server/public/model" "github.com/mattermost/mattermost/server/public/shared/i18n" "github.com/mattermost/mattermost/server/public/shared/mlog" + "github.com/mattermost/mattermost/server/public/shared/request" "github.com/mattermost/mattermost/server/v8/channels/app" - "github.com/mattermost/mattermost/server/v8/channels/app/request" "github.com/mattermost/mattermost/server/v8/channels/utils" ) diff --git a/server/channels/app/slashcommands/command_logout.go b/server/channels/app/slashcommands/command_logout.go index 376698b14c9..0263b83bb24 100644 --- a/server/channels/app/slashcommands/command_logout.go +++ b/server/channels/app/slashcommands/command_logout.go @@ -6,8 +6,8 @@ package slashcommands import ( "github.com/mattermost/mattermost/server/public/model" "github.com/mattermost/mattermost/server/public/shared/i18n" + "github.com/mattermost/mattermost/server/public/shared/request" "github.com/mattermost/mattermost/server/v8/channels/app" - "github.com/mattermost/mattermost/server/v8/channels/app/request" ) type LogoutProvider struct { diff --git a/server/channels/app/slashcommands/command_marketplace.go b/server/channels/app/slashcommands/command_marketplace.go index 1da64332793..7e36c91ca8e 100644 --- a/server/channels/app/slashcommands/command_marketplace.go +++ b/server/channels/app/slashcommands/command_marketplace.go @@ -6,8 +6,8 @@ package slashcommands import ( "github.com/mattermost/mattermost/server/public/model" "github.com/mattermost/mattermost/server/public/shared/i18n" + "github.com/mattermost/mattermost/server/public/shared/request" "github.com/mattermost/mattermost/server/v8/channels/app" - "github.com/mattermost/mattermost/server/v8/channels/app/request" ) type MarketplaceProvider struct { diff --git a/server/channels/app/slashcommands/command_me.go b/server/channels/app/slashcommands/command_me.go index c11424df2de..794f3bddc97 100644 --- a/server/channels/app/slashcommands/command_me.go +++ b/server/channels/app/slashcommands/command_me.go @@ -6,8 +6,8 @@ package slashcommands import ( "github.com/mattermost/mattermost/server/public/model" "github.com/mattermost/mattermost/server/public/shared/i18n" + "github.com/mattermost/mattermost/server/public/shared/request" "github.com/mattermost/mattermost/server/v8/channels/app" - "github.com/mattermost/mattermost/server/v8/channels/app/request" ) type MeProvider struct { diff --git a/server/channels/app/slashcommands/command_msg.go b/server/channels/app/slashcommands/command_msg.go index 80272f1cf18..297139ea12a 100644 --- a/server/channels/app/slashcommands/command_msg.go +++ b/server/channels/app/slashcommands/command_msg.go @@ -10,8 +10,8 @@ import ( "github.com/mattermost/mattermost/server/public/model" "github.com/mattermost/mattermost/server/public/shared/i18n" "github.com/mattermost/mattermost/server/public/shared/mlog" + "github.com/mattermost/mattermost/server/public/shared/request" "github.com/mattermost/mattermost/server/v8/channels/app" - "github.com/mattermost/mattermost/server/v8/channels/app/request" "github.com/mattermost/mattermost/server/v8/channels/store" ) diff --git a/server/channels/app/slashcommands/command_mute.go b/server/channels/app/slashcommands/command_mute.go index 4e47aa6d51e..fd5e6894b18 100644 --- a/server/channels/app/slashcommands/command_mute.go +++ b/server/channels/app/slashcommands/command_mute.go @@ -8,8 +8,8 @@ import ( "github.com/mattermost/mattermost/server/public/model" "github.com/mattermost/mattermost/server/public/shared/i18n" + "github.com/mattermost/mattermost/server/public/shared/request" "github.com/mattermost/mattermost/server/v8/channels/app" - "github.com/mattermost/mattermost/server/v8/channels/app/request" ) type MuteProvider struct { diff --git a/server/channels/app/slashcommands/command_offline.go b/server/channels/app/slashcommands/command_offline.go index 26346e0171c..8b013084cdf 100644 --- a/server/channels/app/slashcommands/command_offline.go +++ b/server/channels/app/slashcommands/command_offline.go @@ -6,8 +6,8 @@ package slashcommands import ( "github.com/mattermost/mattermost/server/public/model" "github.com/mattermost/mattermost/server/public/shared/i18n" + "github.com/mattermost/mattermost/server/public/shared/request" "github.com/mattermost/mattermost/server/v8/channels/app" - "github.com/mattermost/mattermost/server/v8/channels/app/request" ) type OfflineProvider struct { diff --git a/server/channels/app/slashcommands/command_online.go b/server/channels/app/slashcommands/command_online.go index 68d7f99ceb8..aaa8a9c0e2e 100644 --- a/server/channels/app/slashcommands/command_online.go +++ b/server/channels/app/slashcommands/command_online.go @@ -6,8 +6,8 @@ package slashcommands import ( "github.com/mattermost/mattermost/server/public/model" "github.com/mattermost/mattermost/server/public/shared/i18n" + "github.com/mattermost/mattermost/server/public/shared/request" "github.com/mattermost/mattermost/server/v8/channels/app" - "github.com/mattermost/mattermost/server/v8/channels/app/request" ) type OnlineProvider struct { diff --git a/server/channels/app/slashcommands/command_remote.go b/server/channels/app/slashcommands/command_remote.go index de3e9ca7edf..8568a530183 100644 --- a/server/channels/app/slashcommands/command_remote.go +++ b/server/channels/app/slashcommands/command_remote.go @@ -11,8 +11,8 @@ import ( "github.com/mattermost/mattermost/server/public/model" "github.com/mattermost/mattermost/server/public/shared/i18n" + "github.com/mattermost/mattermost/server/public/shared/request" "github.com/mattermost/mattermost/server/v8/channels/app" - "github.com/mattermost/mattermost/server/v8/channels/app/request" ) const ( diff --git a/server/channels/app/slashcommands/command_remove.go b/server/channels/app/slashcommands/command_remove.go index e429706ccc7..a34906e98a9 100644 --- a/server/channels/app/slashcommands/command_remove.go +++ b/server/channels/app/slashcommands/command_remove.go @@ -9,8 +9,8 @@ import ( "github.com/mattermost/mattermost/server/public/model" "github.com/mattermost/mattermost/server/public/shared/i18n" "github.com/mattermost/mattermost/server/public/shared/mlog" + "github.com/mattermost/mattermost/server/public/shared/request" "github.com/mattermost/mattermost/server/v8/channels/app" - "github.com/mattermost/mattermost/server/v8/channels/app/request" ) type RemoveProvider struct { diff --git a/server/channels/app/slashcommands/command_search.go b/server/channels/app/slashcommands/command_search.go index a2d505aac89..b7b3dacfc32 100644 --- a/server/channels/app/slashcommands/command_search.go +++ b/server/channels/app/slashcommands/command_search.go @@ -6,8 +6,8 @@ package slashcommands import ( "github.com/mattermost/mattermost/server/public/model" "github.com/mattermost/mattermost/server/public/shared/i18n" + "github.com/mattermost/mattermost/server/public/shared/request" "github.com/mattermost/mattermost/server/v8/channels/app" - "github.com/mattermost/mattermost/server/v8/channels/app/request" ) type SearchProvider struct { diff --git a/server/channels/app/slashcommands/command_settings.go b/server/channels/app/slashcommands/command_settings.go index b6710a7ef10..96071729c04 100644 --- a/server/channels/app/slashcommands/command_settings.go +++ b/server/channels/app/slashcommands/command_settings.go @@ -6,8 +6,8 @@ package slashcommands import ( "github.com/mattermost/mattermost/server/public/model" "github.com/mattermost/mattermost/server/public/shared/i18n" + "github.com/mattermost/mattermost/server/public/shared/request" "github.com/mattermost/mattermost/server/v8/channels/app" - "github.com/mattermost/mattermost/server/v8/channels/app/request" ) type SettingsProvider struct { diff --git a/server/channels/app/slashcommands/command_share.go b/server/channels/app/slashcommands/command_share.go index a680515b9c0..629c5debd52 100644 --- a/server/channels/app/slashcommands/command_share.go +++ b/server/channels/app/slashcommands/command_share.go @@ -10,8 +10,8 @@ import ( "github.com/mattermost/mattermost/server/public/model" "github.com/mattermost/mattermost/server/public/shared/i18n" + "github.com/mattermost/mattermost/server/public/shared/request" "github.com/mattermost/mattermost/server/v8/channels/app" - "github.com/mattermost/mattermost/server/v8/channels/app/request" ) type ShareProvider struct { diff --git a/server/channels/app/slashcommands/command_shortcuts.go b/server/channels/app/slashcommands/command_shortcuts.go index 019c6a341ac..26b2b5f7065 100644 --- a/server/channels/app/slashcommands/command_shortcuts.go +++ b/server/channels/app/slashcommands/command_shortcuts.go @@ -6,8 +6,8 @@ package slashcommands import ( "github.com/mattermost/mattermost/server/public/model" "github.com/mattermost/mattermost/server/public/shared/i18n" + "github.com/mattermost/mattermost/server/public/shared/request" "github.com/mattermost/mattermost/server/v8/channels/app" - "github.com/mattermost/mattermost/server/v8/channels/app/request" ) type ShortcutsProvider struct { diff --git a/server/channels/app/slashcommands/command_shrug.go b/server/channels/app/slashcommands/command_shrug.go index 2685485447e..3af4dbefb01 100644 --- a/server/channels/app/slashcommands/command_shrug.go +++ b/server/channels/app/slashcommands/command_shrug.go @@ -6,8 +6,8 @@ package slashcommands import ( "github.com/mattermost/mattermost/server/public/model" "github.com/mattermost/mattermost/server/public/shared/i18n" + "github.com/mattermost/mattermost/server/public/shared/request" "github.com/mattermost/mattermost/server/v8/channels/app" - "github.com/mattermost/mattermost/server/v8/channels/app/request" ) type ShrugProvider struct { diff --git a/server/channels/app/slashcommands/helper_test.go b/server/channels/app/slashcommands/helper_test.go index bd0f88668ad..a7a639bdb6e 100644 --- a/server/channels/app/slashcommands/helper_test.go +++ b/server/channels/app/slashcommands/helper_test.go @@ -15,8 +15,8 @@ import ( "github.com/mattermost/mattermost/server/public/model" "github.com/mattermost/mattermost/server/public/shared/mlog" + "github.com/mattermost/mattermost/server/public/shared/request" "github.com/mattermost/mattermost/server/v8/channels/app" - "github.com/mattermost/mattermost/server/v8/channels/app/request" "github.com/mattermost/mattermost/server/v8/channels/store" "github.com/mattermost/mattermost/server/v8/config" ) diff --git a/server/channels/app/status.go b/server/channels/app/status.go index 0fbc15eb180..f5129f36013 100644 --- a/server/channels/app/status.go +++ b/server/channels/app/status.go @@ -9,7 +9,7 @@ import ( "github.com/mattermost/mattermost/server/public/model" "github.com/mattermost/mattermost/server/public/shared/mlog" - "github.com/mattermost/mattermost/server/v8/channels/app/request" + "github.com/mattermost/mattermost/server/public/shared/request" ) // GetUserStatusesByIds used by apiV4 diff --git a/server/channels/app/support_packet.go b/server/channels/app/support_packet.go index c1b83cb0da7..ca979ec8b4d 100644 --- a/server/channels/app/support_packet.go +++ b/server/channels/app/support_packet.go @@ -4,20 +4,29 @@ package app import ( + "bytes" "encoding/json" - "fmt" "os" "runtime" + "runtime/pprof" "strings" + "time" + "github.com/hashicorp/go-multierror" "github.com/pkg/errors" "gopkg.in/yaml.v2" "github.com/mattermost/mattermost/server/public/model" + "github.com/mattermost/mattermost/server/public/shared/mlog" + "github.com/mattermost/mattermost/server/public/shared/request" "github.com/mattermost/mattermost/server/v8/config" ) -func (a *App) GenerateSupportPacket() []model.FileData { +const ( + cpuProfileDuration = 5 * time.Second +) + +func (a *App) GenerateSupportPacket(c *request.Context) []model.FileData { // If any errors we come across within this function, we will log it in a warning.txt file so that we know why certain files did not get produced if any var warnings []string @@ -25,21 +34,24 @@ func (a *App) GenerateSupportPacket() []model.FileData { fileDatas := []model.FileData{} // A array of the functions that we can iterate through since they all have the same return value - functions := []func() (*model.FileData, string){ - a.generateSupportPacketYaml, - a.createPluginsFile, - a.createSanitizedConfigFile, - a.getMattermostLog, - a.getNotificationsLog, + functions := map[string]func(c *request.Context) (*model.FileData, error){ + "support package": a.generateSupportPacketYaml, + "plugins": a.createPluginsFile, + "config": a.createSanitizedConfigFile, + "mattermost log": a.getMattermostLog, + "notification log": a.getNotificationsLog, + "cpu profile": a.createCPUProfile, + "heap profile": a.createHeapProfile, + "goroutines": a.createGoroutineProfile, } - for _, fn := range functions { - fileData, warning := fn() - - if fileData != nil { + for name, fn := range functions { + fileData, err := fn(c) + if err != nil { + mlog.Error("Failed to generate file for support package", mlog.Err(err), mlog.String("file", name)) + warnings = append(warnings, err.Error()) + } else if fileData != nil { fileDatas = append(fileDatas, *fileData) - } else { - warnings = append(warnings, warning) } } @@ -55,8 +67,39 @@ func (a *App) GenerateSupportPacket() []model.FileData { return fileDatas } -func (a *App) generateSupportPacketYaml() (*model.FileData, string) { - // Here we are getting information regarding Elastic Search +func (a *App) generateSupportPacketYaml(c *request.Context) (*model.FileData, error) { + var rErr error + + /* DB */ + + databaseType, databaseSchemaVersion := a.Srv().DatabaseTypeAndSchemaVersion() + databaseVersion, _ := a.Srv().Store().GetDbVersion(false) + + /* Cluster */ + + var clusterID string + if a.Cluster() != nil { + clusterID = a.Cluster().GetClusterId() + } + + /* File store */ + + fileDriver := a.Srv().Platform().FileBackend().DriverName() + fileStatus := model.StatusOk + err := a.Srv().Platform().FileBackend().TestConnection() + if err != nil { + fileStatus = model.StatusFail + ": " + err.Error() + } + + /* LDAP */ + + var vendorName, vendorVersion string + if ldapInterface := a.ch.Ldap; a.ch.Ldap != nil { + vendorName, vendorVersion = ldapInterface.GetVendorNameAndVendorVersion() + } + + /* Elastic Search */ + var elasticServerVersion string var elasticServerPlugins []string if a.Srv().Platform().SearchEngine.ElasticsearchEngine != nil { @@ -64,35 +107,7 @@ func (a *App) generateSupportPacketYaml() (*model.FileData, string) { elasticServerPlugins = a.Srv().Platform().SearchEngine.ElasticsearchEngine.GetPlugins() } - // Here we are getting information regarding LDAP - ldapInterface := a.ch.Ldap - var vendorName, vendorVersion string - if ldapInterface != nil { - vendorName, vendorVersion = ldapInterface.GetVendorNameAndVendorVersion() - } - - // Here we are getting information regarding the database (mysql/postgres + current schema version) - databaseType, databaseSchemaVersion := a.Srv().DatabaseTypeAndSchemaVersion() - - databaseVersion, _ := a.Srv().Store().GetDbVersion(false) - - uniqueUserCount, err := a.Srv().Store().User().Count(model.UserCountOptions{}) - if err != nil { - return nil, errors.Wrap(err, "error while getting user count").Error() - } - - analytics, err := a.GetAnalytics("standard", "") - if analytics == nil { - return nil, errors.Wrap(err, "error while getting analytics").Error() - } - - elasticPostIndexing, _ := a.Srv().Store().Job().GetAllByTypePage("elasticsearch_post_indexing", 0, 2) - elasticPostAggregation, _ := a.Srv().Store().Job().GetAllByTypePage("elasticsearch_post_aggregation", 0, 2) - ldapSyncJobs, _ := a.Srv().Store().Job().GetAllByTypePage("ldap_sync", 0, 2) - messageExport, _ := a.Srv().Store().Job().GetAllByTypePage("message_export", 0, 2) - dataRetentionJobs, _ := a.Srv().Store().Job().GetAllByTypePage("data_retention", 0, 2) - complianceJobs, _ := a.Srv().Store().Job().GetAllByTypePage("compliance", 0, 2) - migrationJobs, _ := a.Srv().Store().Job().GetAllByTypePage("migrations", 0, 2) + /* License */ licenseTo := "" supportedUsers := 0 @@ -101,142 +116,236 @@ func (a *App) generateSupportPacketYaml() (*model.FileData, string) { licenseTo = license.Customer.Company } + /* Jobs */ + + uniqueUserCount, err := a.Srv().Store().User().Count(model.UserCountOptions{}) + if err != nil { + rErr = multierror.Append(errors.Wrap(err, "error while getting user count")) + } + + dataRetentionJobs, err := a.Srv().Store().Job().GetAllByTypePage(c, model.JobTypeDataRetention, 0, 2) + if err != nil { + rErr = multierror.Append(errors.Wrap(err, "error while getting data retention jobs")) + } + messageExportJobs, err := a.Srv().Store().Job().GetAllByTypePage(c, model.JobTypeMessageExport, 0, 2) + if err != nil { + rErr = multierror.Append(errors.Wrap(err, "error while getting message export jobs")) + } + elasticPostIndexingJobs, err := a.Srv().Store().Job().GetAllByTypePage(c, model.JobTypeElasticsearchPostIndexing, 0, 2) + if err != nil { + rErr = multierror.Append(errors.Wrap(err, "error while getting ES post indexing jobs")) + } + elasticPostAggregationJobs, _ := a.Srv().Store().Job().GetAllByTypePage(c, model.JobTypeElasticsearchPostAggregation, 0, 2) + if err != nil { + rErr = multierror.Append(errors.Wrap(err, "error while getting ES post aggregation jobs")) + } + blevePostIndexingJobs, _ := a.Srv().Store().Job().GetAllByTypePage(c, model.JobTypeBlevePostIndexing, 0, 2) + if err != nil { + rErr = multierror.Append(errors.Wrap(err, "error while getting bleve post indexing jobs")) + } + ldapSyncJobs, err := a.Srv().Store().Job().GetAllByTypePage(c, model.JobTypeLdapSync, 0, 2) + if err != nil { + rErr = multierror.Append(errors.Wrap(err, "error while getting LDAP sync jobs")) + } + migrationJobs, err := a.Srv().Store().Job().GetAllByTypePage(c, model.JobTypeMigrations, 0, 2) + if err != nil { + rErr = multierror.Append(errors.Wrap(err, "error while getting migration jobs")) + } + // Creating the struct for support packet yaml file supportPacket := model.SupportPacket{ - LicenseTo: licenseTo, - ServerOS: runtime.GOOS, - ServerArchitecture: runtime.GOARCH, - ServerVersion: model.CurrentVersion, - BuildHash: model.BuildHash, - DatabaseType: databaseType, - DatabaseVersion: databaseVersion, - DatabaseSchemaVersion: databaseSchemaVersion, - LdapVendorName: vendorName, - LdapVendorVersion: vendorVersion, - ElasticServerVersion: elasticServerVersion, - ElasticServerPlugins: elasticServerPlugins, - ActiveUsers: int(uniqueUserCount), - LicenseSupportedUsers: supportedUsers, - TotalChannels: int(analytics[0].Value) + int(analytics[1].Value), - TotalPosts: int(analytics[2].Value), - TotalTeams: int(analytics[4].Value), - WebsocketConnections: int(analytics[5].Value), - MasterDbConnections: int(analytics[6].Value), - ReplicaDbConnections: int(analytics[7].Value), - DailyActiveUsers: int(analytics[8].Value), - MonthlyActiveUsers: int(analytics[9].Value), - InactiveUserCount: int(analytics[10].Value), - ElasticPostIndexingJobs: elasticPostIndexing, - ElasticPostAggregationJobs: elasticPostAggregation, - LdapSyncJobs: ldapSyncJobs, - MessageExportJobs: messageExport, + /* Build information */ + ServerOS: runtime.GOOS, + ServerArchitecture: runtime.GOARCH, + ServerVersion: model.CurrentVersion, + BuildHash: model.BuildHash, + + /* DB */ + DatabaseType: databaseType, + DatabaseVersion: databaseVersion, + DatabaseSchemaVersion: databaseSchemaVersion, + + /* Cluster */ + ClusterID: clusterID, + + /* File store */ + FileDriver: fileDriver, + FileStatus: fileStatus, + + /* LDAP */ + LdapVendorName: vendorName, + LdapVendorVersion: vendorVersion, + + /* Elastic Search */ + ElasticServerVersion: elasticServerVersion, + ElasticServerPlugins: elasticServerPlugins, + + /* License */ + LicenseTo: licenseTo, + LicenseSupportedUsers: supportedUsers, + + /* Server stats */ + ActiveUsers: int(uniqueUserCount), + + /* Jobs */ DataRetentionJobs: dataRetentionJobs, - ComplianceJobs: complianceJobs, + MessageExportJobs: messageExportJobs, + ElasticPostIndexingJobs: elasticPostIndexingJobs, + ElasticPostAggregationJobs: elasticPostAggregationJobs, + BlevePostIndexingJobs: blevePostIndexingJobs, + LdapSyncJobs: ldapSyncJobs, MigrationJobs: migrationJobs, } + /* Server stats */ + + analytics, appErr := a.GetAnalytics("standard", "") + if appErr != nil { + rErr = multierror.Append(errors.Wrap(appErr, "error while getting analytics")) + } + if len(analytics) < 11 { + rErr = multierror.Append(errors.New("not enought analytics information found")) + } else { + supportPacket.TotalChannels = int(analytics[0].Value) + int(analytics[1].Value) + supportPacket.TotalPosts = int(analytics[2].Value) + supportPacket.TotalTeams = int(analytics[4].Value) + supportPacket.WebsocketConnections = int(analytics[5].Value) + supportPacket.MasterDbConnections = int(analytics[6].Value) + supportPacket.ReplicaDbConnections = int(analytics[7].Value) + supportPacket.DailyActiveUsers = int(analytics[8].Value) + supportPacket.MonthlyActiveUsers = int(analytics[9].Value) + supportPacket.InactiveUserCount = int(analytics[10].Value) + } + // Marshal to a Yaml File supportPacketYaml, err := yaml.Marshal(&supportPacket) - if err == nil { - fileData := model.FileData{ - Filename: "support_packet.yaml", - Body: supportPacketYaml, - } - return &fileData, "" + if err != nil { + rErr = multierror.Append(errors.Wrap(err, "failed to marshal support package into yaml")) } - warning := fmt.Sprintf("yaml.Marshal(&supportPacket) Error: %s", err.Error()) - return nil, warning + fileData := &model.FileData{ + Filename: "support_packet.yaml", + Body: supportPacketYaml, + } + return fileData, rErr } -func (a *App) createPluginsFile() (*model.FileData, string) { - var warning string +func (a *App) createPluginsFile(_ *request.Context) (*model.FileData, error) { // Getting the plugins installed on the server, prettify it, and then add them to the file data array pluginsResponse, appErr := a.GetPlugins() - if appErr == nil { - pluginsPrettyJSON, err := json.MarshalIndent(pluginsResponse, "", " ") - if err == nil { - fileData := model.FileData{ - Filename: "plugins.json", - Body: pluginsPrettyJSON, - } - - return &fileData, "" - } - - warning = fmt.Sprintf("json.MarshalIndent(pluginsResponse) Error: %s", err.Error()) - } else { - warning = fmt.Sprintf("c.App.GetPlugins() Error: %s", appErr.Error()) + if appErr != nil { + return nil, errors.Wrap(appErr, "failed to get plugin list for support package") } - return nil, warning -} - -func (a *App) getNotificationsLog() (*model.FileData, string) { - var warning string - - // Getting notifications.log - if *a.Config().NotificationLogSettings.EnableFile { - // notifications.log - notificationsLog := config.GetNotificationsLogFileLocation(*a.Config().LogSettings.FileLocation) - - notificationsLogFileData, notificationsLogFileDataErr := os.ReadFile(notificationsLog) - - if notificationsLogFileDataErr == nil { - fileData := model.FileData{ - Filename: "notifications.log", - Body: notificationsLogFileData, - } - return &fileData, "" - } - - warning = fmt.Sprintf("os.ReadFile(notificationsLog) Error: %s", notificationsLogFileDataErr.Error()) - - } else { - warning = "Unable to retrieve notifications.log because LogSettings: EnableFile is false in config.json" + pluginsPrettyJSON, err := json.MarshalIndent(pluginsResponse, "", " ") + if err != nil { + return nil, errors.Wrap(err, "failed to marshal plugin list into json") } - return nil, warning + fileData := &model.FileData{ + Filename: "plugins.json", + Body: pluginsPrettyJSON, + } + return fileData, nil + } -func (a *App) getMattermostLog() (*model.FileData, string) { - var warning string - - // Getting mattermost.log - if *a.Config().LogSettings.EnableFile { - // mattermost.log - mattermostLog := config.GetLogFileLocation(*a.Config().LogSettings.FileLocation) - - mattermostLogFileData, mattermostLogFileDataErr := os.ReadFile(mattermostLog) - - if mattermostLogFileDataErr == nil { - fileData := model.FileData{ - Filename: "mattermost.log", - Body: mattermostLogFileData, - } - return &fileData, "" - } - warning = fmt.Sprintf("os.ReadFile(mattermostLog) Error: %s", mattermostLogFileDataErr.Error()) - - } else { - warning = "Unable to retrieve mattermost.log because LogSettings: EnableFile is false in config.json" +func (a *App) getNotificationsLog(_ *request.Context) (*model.FileData, error) { + if !*a.Config().NotificationLogSettings.EnableFile { + return nil, errors.New("Unable to retrieve notifications.log because LogSettings: EnableFile is set to false") } - return nil, warning + notificationsLog := config.GetNotificationsLogFileLocation(*a.Config().LogSettings.FileLocation) + notificationsLogFileData, err := os.ReadFile(notificationsLog) + if err != nil { + return nil, errors.Wrapf(err, "failed read notifcation log file at path %s", notificationsLog) + } + + fileData := &model.FileData{ + Filename: "notifications.log", + Body: notificationsLogFileData, + } + return fileData, nil } -func (a *App) createSanitizedConfigFile() (*model.FileData, string) { +func (a *App) getMattermostLog(_ *request.Context) (*model.FileData, error) { + if !*a.Config().LogSettings.EnableFile { + return nil, errors.New("Unable to retrieve mattermost.log because LogSettings: EnableFile is set to false") + } + + mattermostLog := config.GetLogFileLocation(*a.Config().LogSettings.FileLocation) + mattermostLogFileData, err := os.ReadFile(mattermostLog) + if err != nil { + return nil, errors.Wrapf(err, "failed read mattermost log file at path %s", mattermostLog) + } + + fileData := &model.FileData{ + Filename: "mattermost.log", + Body: mattermostLogFileData, + } + return fileData, nil +} + +func (a *App) createSanitizedConfigFile(_ *request.Context) (*model.FileData, error) { // Getting sanitized config, prettifying it, and then adding it to our file data array sanitizedConfigPrettyJSON, err := json.MarshalIndent(a.GetSanitizedConfig(), "", " ") - if err == nil { - fileData := model.FileData{ - Filename: "sanitized_config.json", - Body: sanitizedConfigPrettyJSON, - } - return &fileData, "" + if err != nil { + return nil, errors.Wrap(err, "failed to sanitized config into json") } - warning := fmt.Sprintf("json.MarshalIndent(c.App.GetSanitizedConfig()) Error: %s", err.Error()) - return nil, warning + fileData := &model.FileData{ + Filename: "sanitized_config.json", + Body: sanitizedConfigPrettyJSON, + } + return fileData, nil +} + +func (a *App) createCPUProfile(_ *request.Context) (*model.FileData, error) { + var b bytes.Buffer + + err := pprof.StartCPUProfile(&b) + if err != nil { + return nil, errors.Wrap(err, "failed to start CPU profile") + } + + time.Sleep(cpuProfileDuration) + + pprof.StopCPUProfile() + + fileData := &model.FileData{ + Filename: "cpu.prof", + Body: b.Bytes(), + } + return fileData, nil +} + +func (a *App) createHeapProfile(*request.Context) (*model.FileData, error) { + var b bytes.Buffer + + err := pprof.Lookup("heap").WriteTo(&b, 0) + if err != nil { + return nil, errors.Wrap(err, "failed to lookup heap profile") + } + + fileData := &model.FileData{ + Filename: "heap.prof", + Body: b.Bytes(), + } + return fileData, nil +} + +func (a *App) createGoroutineProfile(_ *request.Context) (*model.FileData, error) { + var b bytes.Buffer + + err := pprof.Lookup("goroutine").WriteTo(&b, 2) + if err != nil { + return nil, errors.Wrap(err, "failed to lookup goroutine profile") + } + + fileData := &model.FileData{ + Filename: "goroutines", + Body: b.Bytes(), + } + return fileData, nil } diff --git a/server/channels/app/support_packet_test.go b/server/channels/app/support_packet_test.go index 09e93e1ef8b..d025ba2e77f 100644 --- a/server/channels/app/support_packet_test.go +++ b/server/channels/app/support_packet_test.go @@ -4,6 +4,7 @@ package app import ( + "errors" "os" "testing" @@ -12,18 +13,22 @@ import ( "gopkg.in/yaml.v2" "github.com/mattermost/mattermost/server/public/model" + "github.com/mattermost/mattermost/server/public/shared/request" + "github.com/mattermost/mattermost/server/v8/channels/app/platform" + fmocks "github.com/mattermost/mattermost/server/v8/platform/shared/filestore/mocks" ) func TestCreatePluginsFile(t *testing.T) { th := Setup(t) defer th.TearDown() + ctx := request.EmptyContext(th.TestLogger) - // Happy path where we have a plugins file with no warning - fileData, warning := th.App.createPluginsFile() + // Happy path where we have a plugins file with no err + fileData, err := th.App.createPluginsFile(ctx) require.NotNil(t, fileData) assert.Equal(t, "plugins.json", fileData.Filename) assert.Positive(t, len(fileData.Body)) - assert.Empty(t, warning) + assert.NoError(t, err) // Turn off plugins so we can get an error th.App.UpdateConfig(func(cfg *model.Config) { @@ -31,34 +36,63 @@ func TestCreatePluginsFile(t *testing.T) { }) // Plugins off in settings so no fileData and we get a warning instead - fileData, warning = th.App.createPluginsFile() + fileData, err = th.App.createPluginsFile(ctx) assert.Nil(t, fileData) - assert.Contains(t, warning, "c.App.GetPlugins() Error:") + assert.ErrorContains(t, err, "failed to get plugin list for support package") } func TestGenerateSupportPacketYaml(t *testing.T) { th := Setup(t).InitBasic() defer th.TearDown() + ctx := request.EmptyContext(th.TestLogger) licenseUsers := 100 license := model.NewTestLicense() license.Features.Users = model.NewInt(licenseUsers) th.App.Srv().SetLicense(license) - // Happy path where we have a support packet yaml file without any warnings - fileData, warning := th.App.generateSupportPacketYaml() - require.NotNil(t, fileData) - assert.Equal(t, "support_packet.yaml", fileData.Filename) - assert.Positive(t, len(fileData.Body)) - assert.Empty(t, warning) - var packet model.SupportPacket - require.NoError(t, yaml.Unmarshal(fileData.Body, &packet)) - assert.Equal(t, 3, packet.ActiveUsers) // from InitBasic. - assert.Equal(t, licenseUsers, packet.LicenseSupportedUsers) + t.Run("Happy path", func(t *testing.T) { + // Happy path where we have a support packet yaml file without any warnings + + fileData, err := th.App.generateSupportPacketYaml(ctx) + require.NotNil(t, fileData) + assert.Equal(t, "support_packet.yaml", fileData.Filename) + assert.Positive(t, len(fileData.Body)) + assert.NoError(t, err) + var packet model.SupportPacket + require.NoError(t, yaml.Unmarshal(fileData.Body, &packet)) + + assert.Equal(t, 3, packet.ActiveUsers) // from InitBasic. + assert.Equal(t, licenseUsers, packet.LicenseSupportedUsers) + assert.Empty(t, packet.ClusterID) + assert.Equal(t, "local", packet.FileDriver) + assert.Equal(t, "OK", packet.FileStatus) + }) + + t.Run("filestore fails", func(t *testing.T) { + fb := &fmocks.FileBackend{} + platform.SetFileStore(fb)(th.Server.Platform()) + fb.On("DriverName").Return("mock") + fb.On("TestConnection").Return(errors.New("all broken")) + + fileData, err := th.App.generateSupportPacketYaml(ctx) + require.NotNil(t, fileData) + assert.Equal(t, "support_packet.yaml", fileData.Filename) + assert.Positive(t, len(fileData.Body)) + assert.NoError(t, err) + var packet model.SupportPacket + require.NoError(t, yaml.Unmarshal(fileData.Body, &packet)) + + assert.Equal(t, "mock", packet.FileDriver) + assert.Equal(t, "FAIL: all broken", packet.FileStatus) + }) + } + func TestGenerateSupportPacket(t *testing.T) { th := Setup(t) defer th.TearDown() + ctx := request.EmptyContext(th.TestLogger) d1 := []byte("hello\ngo\n") err := os.WriteFile("mattermost.log", d1, 0777) @@ -66,40 +100,64 @@ func TestGenerateSupportPacket(t *testing.T) { err = os.WriteFile("notifications.log", d1, 0777) require.NoError(t, err) - fileDatas := th.App.GenerateSupportPacket() - testFiles := []string{"support_packet.yaml", "plugins.json", "sanitized_config.json", "mattermost.log", "notifications.log"} - for i, fileData := range fileDatas { - require.NotNil(t, fileData) - assert.Equal(t, testFiles[i], fileData.Filename) - assert.Positive(t, len(fileData.Body)) + fileDatas := th.App.GenerateSupportPacket(ctx) + var rFileNames []string + testFiles := []string{ + "support_packet.yaml", + "plugins.json", + "sanitized_config.json", + "mattermost.log", + "notifications.log", + "cpu.prof", + "heap.prof", + "goroutines", } + for _, fileData := range fileDatas { + require.NotNil(t, fileData) + assert.Positive(t, len(fileData.Body)) + + rFileNames = append(rFileNames, fileData.Filename) + } + assert.ElementsMatch(t, testFiles, rFileNames) // Remove these two files and ensure that warning.txt file is generated err = os.Remove("notifications.log") require.NoError(t, err) err = os.Remove("mattermost.log") require.NoError(t, err) - fileDatas = th.App.GenerateSupportPacket() - testFiles = []string{"support_packet.yaml", "plugins.json", "sanitized_config.json", "warning.txt"} - for i, fileData := range fileDatas { - require.NotNil(t, fileData) - assert.Equal(t, testFiles[i], fileData.Filename) - assert.Positive(t, len(fileData.Body)) + fileDatas = th.App.GenerateSupportPacket(ctx) + testFiles = []string{ + "support_packet.yaml", + "plugins.json", + "sanitized_config.json", + "cpu.prof", + "heap.prof", + "warning.txt", + "goroutines", } + rFileNames = nil + for _, fileData := range fileDatas { + require.NotNil(t, fileData) + assert.Positive(t, len(fileData.Body)) + + rFileNames = append(rFileNames, fileData.Filename) + } + assert.ElementsMatch(t, testFiles, rFileNames) } func TestGetNotificationsLog(t *testing.T) { th := Setup(t) defer th.TearDown() + ctx := request.EmptyContext(th.TestLogger) // Disable notifications file to get an error th.App.UpdateConfig(func(cfg *model.Config) { *cfg.NotificationLogSettings.EnableFile = false }) - fileData, warning := th.App.getNotificationsLog() + fileData, err := th.App.getNotificationsLog(ctx) assert.Nil(t, fileData) - assert.Equal(t, warning, "Unable to retrieve notifications.log because LogSettings: EnableFile is false in config.json") + assert.ErrorContains(t, err, "Unable to retrieve notifications.log because LogSettings: EnableFile is set to false") // Enable notifications file but delete any notifications file to get an error trying to read the file th.App.UpdateConfig(func(cfg *model.Config) { @@ -109,35 +167,36 @@ func TestGetNotificationsLog(t *testing.T) { // If any previous notifications.log file, lets delete it os.Remove("notifications.log") - fileData, warning = th.App.getNotificationsLog() + fileData, err = th.App.getNotificationsLog(ctx) assert.Nil(t, fileData) - assert.Contains(t, warning, "os.ReadFile(notificationsLog) Error:") + assert.ErrorContains(t, err, "failed read notifcation log file at path") - // Happy path where we have file and no warning + // Happy path where we have file and no error d1 := []byte("hello\ngo\n") - err := os.WriteFile("notifications.log", d1, 0777) + err = os.WriteFile("notifications.log", d1, 0777) defer os.Remove("notifications.log") require.NoError(t, err) - fileData, warning = th.App.getNotificationsLog() + fileData, err = th.App.getNotificationsLog(ctx) require.NotNil(t, fileData) assert.Equal(t, "notifications.log", fileData.Filename) assert.Positive(t, len(fileData.Body)) - assert.Empty(t, warning) + assert.NoError(t, err) } func TestGetMattermostLog(t *testing.T) { th := Setup(t) defer th.TearDown() + ctx := request.EmptyContext(th.TestLogger) // disable mattermost log file setting in config so we should get an warning th.App.UpdateConfig(func(cfg *model.Config) { *cfg.LogSettings.EnableFile = false }) - fileData, warning := th.App.getMattermostLog() + fileData, err := th.App.getMattermostLog(ctx) assert.Nil(t, fileData) - assert.Equal(t, "Unable to retrieve mattermost.log because LogSettings: EnableFile is false in config.json", warning) + assert.ErrorContains(t, err, "Unable to retrieve mattermost.log because LogSettings: EnableFile is set to false") // We enable the setting but delete any mattermost log file th.App.UpdateConfig(func(cfg *model.Config) { @@ -147,31 +206,32 @@ func TestGetMattermostLog(t *testing.T) { // If any previous mattermost.log file, lets delete it os.Remove("mattermost.log") - fileData, warning = th.App.getMattermostLog() + fileData, err = th.App.getMattermostLog(ctx) assert.Nil(t, fileData) - assert.Contains(t, warning, "os.ReadFile(mattermostLog) Error:") + assert.ErrorContains(t, err, "failed read mattermost log file at path mattermost.log") // Happy path where we get a log file and no warning d1 := []byte("hello\ngo\n") - err := os.WriteFile("mattermost.log", d1, 0777) + err = os.WriteFile("mattermost.log", d1, 0777) defer os.Remove("mattermost.log") require.NoError(t, err) - fileData, warning = th.App.getMattermostLog() + fileData, err = th.App.getMattermostLog(ctx) require.NotNil(t, fileData) assert.Equal(t, "mattermost.log", fileData.Filename) assert.Positive(t, len(fileData.Body)) - assert.Empty(t, warning) + assert.NoError(t, err) } func TestCreateSanitizedConfigFile(t *testing.T) { th := Setup(t) defer th.TearDown() + ctx := request.EmptyContext(th.TestLogger) - // Happy path where we have a sanitized config file with no warning - fileData, warning := th.App.createSanitizedConfigFile() + // Happy path where we have a sanitized config file with no err + fileData, err := th.App.createSanitizedConfigFile(ctx) require.NotNil(t, fileData) assert.Equal(t, "sanitized_config.json", fileData.Filename) assert.Positive(t, len(fileData.Body)) - assert.Empty(t, warning) + assert.NoError(t, err) } diff --git a/server/channels/app/syncables.go b/server/channels/app/syncables.go index 785b6aca93a..5a060f2b97f 100644 --- a/server/channels/app/syncables.go +++ b/server/channels/app/syncables.go @@ -10,7 +10,7 @@ import ( "github.com/mattermost/mattermost/server/public/model" "github.com/mattermost/mattermost/server/public/shared/mlog" - "github.com/mattermost/mattermost/server/v8/channels/app/request" + "github.com/mattermost/mattermost/server/public/shared/request" ) // createDefaultChannelMemberships adds users to channels based on their group memberships and how those groups are diff --git a/server/channels/app/team.go b/server/channels/app/team.go index f95a5c1889c..449acb2b7b5 100644 --- a/server/channels/app/team.go +++ b/server/channels/app/team.go @@ -21,9 +21,9 @@ import ( "github.com/mattermost/mattermost/server/public/plugin" "github.com/mattermost/mattermost/server/public/shared/i18n" "github.com/mattermost/mattermost/server/public/shared/mlog" + "github.com/mattermost/mattermost/server/public/shared/request" "github.com/mattermost/mattermost/server/v8/channels/app/email" "github.com/mattermost/mattermost/server/v8/channels/app/imaging" - "github.com/mattermost/mattermost/server/v8/channels/app/request" "github.com/mattermost/mattermost/server/v8/channels/app/teams" "github.com/mattermost/mattermost/server/v8/channels/app/users" "github.com/mattermost/mattermost/server/v8/channels/product" @@ -2057,21 +2057,21 @@ func (a *App) RemoveTeamIcon(teamID string) *model.AppError { return nil } -func (a *App) InvalidateAllEmailInvites() *model.AppError { +func (a *App) InvalidateAllEmailInvites(c *request.Context) *model.AppError { if err := a.Srv().Store().Token().RemoveAllTokensByType(TokenTypeTeamInvitation); err != nil { return model.NewAppError("InvalidateAllEmailInvites", "api.team.invalidate_all_email_invites.app_error", nil, "", http.StatusInternalServerError).Wrap(err) } if err := a.Srv().Store().Token().RemoveAllTokensByType(TokenTypeGuestInvitation); err != nil { return model.NewAppError("InvalidateAllEmailInvites", "api.team.invalidate_all_email_invites.app_error", nil, "", http.StatusInternalServerError).Wrap(err) } - if err := a.InvalidateAllResendInviteEmailJobs(); err != nil { + if err := a.InvalidateAllResendInviteEmailJobs(c); err != nil { return model.NewAppError("InvalidateAllEmailInvites", "api.team.invalidate_all_email_invites.app_error", nil, "", http.StatusInternalServerError).Wrap(err) } return nil } -func (a *App) InvalidateAllResendInviteEmailJobs() *model.AppError { - jobs, appErr := a.Srv().Jobs.GetJobsByTypeAndStatus(model.JobTypeResendInvitationEmail, model.JobStatusPending) +func (a *App) InvalidateAllResendInviteEmailJobs(c *request.Context) *model.AppError { + jobs, appErr := a.Srv().Jobs.GetJobsByTypeAndStatus(c, model.JobTypeResendInvitationEmail, model.JobStatusPending) if appErr != nil { return appErr } diff --git a/server/channels/app/team_test.go b/server/channels/app/team_test.go index 33478752f06..e869e03f9fc 100644 --- a/server/channels/app/team_test.go +++ b/server/channels/app/team_test.go @@ -19,6 +19,7 @@ import ( "github.com/stretchr/testify/require" "github.com/mattermost/mattermost/server/public/model" + "github.com/mattermost/mattermost/server/public/shared/request" "github.com/mattermost/mattermost/server/v8/channels/app/email" emailmocks "github.com/mattermost/mattermost/server/v8/channels/app/email/mocks" "github.com/mattermost/mattermost/server/v8/channels/app/teams" @@ -1357,18 +1358,19 @@ func TestUpdateTeamMemberRolesChangingGuest(t *testing.T) { func TestInvalidateAllResendInviteEmailJobs(t *testing.T) { th := Setup(t) defer th.TearDown() + ctx := request.EmptyContext(th.TestLogger) - job, err := th.App.Srv().Jobs.CreateJob(model.JobTypeResendInvitationEmail, map[string]string{}) + job, err := th.App.Srv().Jobs.CreateJob(ctx, model.JobTypeResendInvitationEmail, map[string]string{}) require.Nil(t, err) sysVar := &model.System{Name: job.Id, Value: "0"} e := th.App.Srv().Store().System().SaveOrUpdate(sysVar) require.NoError(t, e) - appErr := th.App.InvalidateAllResendInviteEmailJobs() + appErr := th.App.InvalidateAllResendInviteEmailJobs(ctx) require.Nil(t, appErr) - j, e := th.App.Srv().Store().Job().Get(job.Id) + j, e := th.App.Srv().Store().Job().Get(ctx, job.Id) require.NoError(t, e) require.Equal(t, j.Status, model.JobStatusCanceled) @@ -1380,6 +1382,7 @@ func TestInvalidateAllResendInviteEmailJobs(t *testing.T) { func TestInvalidateAllEmailInvites(t *testing.T) { th := Setup(t) defer th.TearDown() + ctx := request.EmptyContext(th.TestLogger) t1 := model.Token{ Token: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", @@ -1408,7 +1411,7 @@ func TestInvalidateAllEmailInvites(t *testing.T) { err = th.App.Srv().Store().Token().Save(&t3) require.NoError(t, err) - appErr := th.App.InvalidateAllEmailInvites() + appErr := th.App.InvalidateAllEmailInvites(ctx) require.Nil(t, appErr) _, err = th.App.Srv().Store().Token().GetByToken(t1.Token) diff --git a/server/channels/app/teams/helper_test.go b/server/channels/app/teams/helper_test.go index a80864a030e..88b65494898 100644 --- a/server/channels/app/teams/helper_test.go +++ b/server/channels/app/teams/helper_test.go @@ -10,7 +10,8 @@ import ( "testing" "github.com/mattermost/mattermost/server/public/model" - "github.com/mattermost/mattermost/server/v8/channels/app/request" + "github.com/mattermost/mattermost/server/public/shared/mlog" + "github.com/mattermost/mattermost/server/public/shared/request" "github.com/mattermost/mattermost/server/v8/channels/store" "github.com/mattermost/mattermost/server/v8/config" ) @@ -80,7 +81,7 @@ func setupTestHelper(s store.Store, includeCacheLayer bool, tb testing.TB) *Test }, wh: &mockWebHub{}, }, - Context: request.EmptyContext(nil), + Context: request.EmptyContext(mlog.CreateConsoleTestLogger(tb)), configStore: configStore, dbStore: s, LogBuffer: buffer, diff --git a/server/channels/app/test_templates/hook_notification_will_be_pushed.tmpl b/server/channels/app/test_templates/hook_notification_will_be_pushed.tmpl new file mode 100644 index 00000000000..1a154e0788f --- /dev/null +++ b/server/channels/app/test_templates/hook_notification_will_be_pushed.tmpl @@ -0,0 +1,18 @@ +package main + +import ( + "github.com/mattermost/mattermost/server/public/model" + "github.com/mattermost/mattermost/server/public/plugin" +) + +type MyPlugin struct { + plugin.MattermostPlugin +} + +func (p *MyPlugin) NotificationWillBePushed(notification *model.PushNotification, userID string) (*model.PushNotification, string) { + %s +} + +func main() { + plugin.ClientMain(&MyPlugin{}) +} diff --git a/server/channels/app/upload.go b/server/channels/app/upload.go index 9b1858744b6..7de74b999c9 100644 --- a/server/channels/app/upload.go +++ b/server/channels/app/upload.go @@ -16,7 +16,7 @@ import ( "github.com/mattermost/mattermost/server/public/model" "github.com/mattermost/mattermost/server/public/plugin" "github.com/mattermost/mattermost/server/public/shared/mlog" - "github.com/mattermost/mattermost/server/v8/channels/app/request" + "github.com/mattermost/mattermost/server/public/shared/request" "github.com/mattermost/mattermost/server/v8/channels/store" ) diff --git a/server/channels/app/user.go b/server/channels/app/user.go index 944443834b0..335f6eed318 100644 --- a/server/channels/app/user.go +++ b/server/channels/app/user.go @@ -23,9 +23,9 @@ import ( "github.com/mattermost/mattermost/server/public/plugin" "github.com/mattermost/mattermost/server/public/shared/i18n" "github.com/mattermost/mattermost/server/public/shared/mlog" + "github.com/mattermost/mattermost/server/public/shared/request" "github.com/mattermost/mattermost/server/v8/channels/app/email" "github.com/mattermost/mattermost/server/v8/channels/app/imaging" - "github.com/mattermost/mattermost/server/v8/channels/app/request" "github.com/mattermost/mattermost/server/v8/channels/app/users" "github.com/mattermost/mattermost/server/v8/channels/store" "github.com/mattermost/mattermost/server/v8/einterfaces" @@ -282,8 +282,9 @@ func (a *App) createUserOrGuest(c request.CTX, user *model.User, guest bool) (*m recommendedNextStepsPref := model.Preference{UserId: ruser.Id, Category: model.PreferenceRecommendedNextSteps, Name: "hide", Value: "false"} tutorialStepPref := model.Preference{UserId: ruser.Id, Category: model.PreferenceCategoryTutorialSteps, Name: ruser.Id, Value: "0"} + gmASdmPref := model.Preference{UserId: ruser.Id, Category: model.PreferenceCategorySystemNotice, Name: "GMasDM", Value: "true"} - preferences := model.Preferences{recommendedNextStepsPref, tutorialStepPref} + preferences := model.Preferences{recommendedNextStepsPref, tutorialStepPref, gmASdmPref} if err := a.Srv().Store().Preference().Save(preferences); err != nil { c.Logger().Warn("Encountered error saving user preferences", mlog.Err(err)) } @@ -308,7 +309,7 @@ func (a *App) createUserOrGuest(c request.CTX, user *model.User, guest bool) (*m // table in CWS. This is then used to calculate how much the customers have to pay in addition for the extra users. If the // workspace is currently on a monthly plan, then this function will not do anything. - if a.Channels().License().IsCloud() { + if a.Channels().License().IsCloud() && !ruser.IsRemote() { go func(userId string) { _, err := a.SendSubscriptionHistoryEvent(userId) if err != nil { @@ -440,6 +441,20 @@ func (a *App) GetUserByEmail(email string) (*model.User, *model.AppError) { return user, nil } +func (a *App) GetUserByRemoteID(remoteID string) (*model.User, *model.AppError) { + user, err := a.ch.srv.userService.GetUserByRemoteID(remoteID) + if err != nil { + var nfErr *store.ErrNotFound + switch { + case errors.As(err, &nfErr): + return nil, model.NewAppError("GetUserByRemoteID", MissingAccountError, nil, "", http.StatusNotFound).Wrap(err) + default: + return nil, model.NewAppError("GetUserByRemoteID", MissingAccountError, nil, "", http.StatusInternalServerError).Wrap(err) + } + } + return user, nil +} + func (a *App) GetUserByAuth(authData *string, authService string) (*model.User, *model.AppError) { user, err := a.ch.srv.userService.GetUserByAuth(authData, authService) if err != nil { @@ -989,6 +1004,16 @@ func (a *App) UpdateActive(c request.CTX, user *model.User, active bool) (*model a.sendUpdatedUserEvent(*ruser) + if pluginsEnvironment := a.GetPluginsEnvironment(); pluginsEnvironment != nil && !active && user.DeleteAt != 0 { + a.Srv().Go(func() { + pluginContext := pluginContext(c) + pluginsEnvironment.RunMultiPluginHook(func(hooks plugin.Hooks) bool { + hooks.UserHasBeenDeactivated(pluginContext, user) + return true + }, plugin.UserHasBeenDeactivatedID) + }) + } + return ruser, nil } @@ -1262,6 +1287,17 @@ func (a *App) UpdateUser(c request.CTX, user *model.User, sendNotifications bool a.InvalidateCacheForUser(user.Id) a.onUserProfileChange(user.Id) + if a.Channels().License().IsCloud() && prev.IsRemote() && !user.IsRemote() { + go func(userId string) { + _, err := a.SendSubscriptionHistoryEvent(userId) + if err != nil { + c.Logger().Error("Failed to create/update the SubscriptionHistoryEvent", mlog.Err(err)) + } + }(user.Id) + } + + newUser.Sanitize(map[string]bool{}) + return newUser, nil } @@ -1665,6 +1701,10 @@ func (a *App) PermanentDeleteUser(c *request.Context, user *model.User) *model.A return model.NewAppError("PermanentDeleteUser", "app.post.permanent_delete_by_user.app_error", nil, "", http.StatusInternalServerError).Wrap(err) } + if err := a.Srv().Store().Reaction().PermanentDeleteByUser(user.Id); err != nil { + return model.NewAppError("PermanentDeleteUser", "app.reaction.permanent_delete_by_user.app_error", nil, "", http.StatusInternalServerError).Wrap(err) + } + if err := a.Srv().Store().Bot().PermanentDelete(user.Id); err != nil { var invErr *store.ErrInvalidInput switch { diff --git a/server/channels/app/user_agent.go b/server/channels/app/user_agent.go index 301edd807a0..42d56c7f054 100644 --- a/server/channels/app/user_agent.go +++ b/server/channels/app/user_agent.go @@ -84,6 +84,11 @@ func getOSName(ua *uasurfer.UserAgent) string { } func getBrowserVersion(ua *uasurfer.UserAgent, userAgentString string) string { + if index := strings.Index(userAgentString, "Mattermost Mobile/"); index != -1 { + afterVersion := userAgentString[index+len("Mattermost Mobile/"):] + return strings.Fields(afterVersion)[0] + } + if index := strings.Index(userAgentString, "Mattermost/"); index != -1 { afterVersion := userAgentString[index+len("Mattermost/"):] return strings.Fields(afterVersion)[0] @@ -123,10 +128,15 @@ var browserNames = map[uasurfer.BrowserName]string{ func getBrowserName(ua *uasurfer.UserAgent, userAgentString string) string { browser := ua.Browser.Name - if strings.Contains(userAgentString, "Mattermost") { + if strings.Contains(userAgentString, "Electron") || + (strings.Contains(userAgentString, "Mattermost") && !strings.Contains(userAgentString, "Mattermost Mobile")) { return "Desktop App" } + if strings.Contains(userAgentString, "Mattermost Mobile") { + return "Mobile App" + } + if strings.Contains(userAgentString, "mmctl") { return "mmctl" } diff --git a/server/channels/app/user_agent_test.go b/server/channels/app/user_agent_test.go index aa730d50fe2..1a40b4dfd92 100644 --- a/server/channels/app/user_agent_test.go +++ b/server/channels/app/user_agent_test.go @@ -33,6 +33,7 @@ var testUserAgents = []testUserAgent{ {"Safari 9", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Safari/604.1.38"}, {"Safari 8", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_4) AppleWebKit/600.7.12 (KHTML, like Gecko) Version/8.0.7 Safari/600.7.12"}, {"Safari Mobile", "Mozilla/5.0 (iPhone; CPU iPhone OS 9_1 like Mac OS X) AppleWebKit/601.1.46 (KHTML, like Gecko) Version/9.0 Mobile/13B137 Safari/601.1"}, + {"Mobile App", "Mattermost Mobile/2.7.0+482 (Android; 13; sdk_gphone64_arm64)"}, } func TestGetPlatformName(t *testing.T) { @@ -53,6 +54,7 @@ func TestGetPlatformName(t *testing.T) { "Macintosh", "Macintosh", "iPhone", + "Linux", } for i, userAgent := range testUserAgents { @@ -83,6 +85,7 @@ func TestGetOSName(t *testing.T) { "Mac OS", "Mac OS", "iOS", + "Android", } for i, userAgent := range testUserAgents { @@ -103,7 +106,7 @@ func TestGetBrowserName(t *testing.T) { "Chrome", "mmctl", "Desktop App", - "Chrome", + "Desktop App", "Edge", "Internet Explorer", "Internet Explorer", @@ -113,6 +116,7 @@ func TestGetBrowserName(t *testing.T) { "Safari", "Safari", "Safari", + "Mobile App", } for i, userAgent := range testUserAgents { @@ -143,6 +147,7 @@ func TestGetBrowserVersion(t *testing.T) { "11.0", "8.0.7", "9.0", + "2.7.0+482", } for i, userAgent := range testUserAgents { diff --git a/server/channels/app/user_test.go b/server/channels/app/user_test.go index 0fd0d439bee..5540af2e542 100644 --- a/server/channels/app/user_test.go +++ b/server/channels/app/user_test.go @@ -19,8 +19,8 @@ import ( "github.com/stretchr/testify/require" "github.com/mattermost/mattermost/server/public/model" + "github.com/mattermost/mattermost/server/public/shared/request" oauthgitlab "github.com/mattermost/mattermost/server/v8/channels/app/oauthproviders/gitlab" - "github.com/mattermost/mattermost/server/v8/channels/app/request" "github.com/mattermost/mattermost/server/v8/channels/app/users" "github.com/mattermost/mattermost/server/v8/channels/store" storemocks "github.com/mattermost/mattermost/server/v8/channels/store/storetest/mocks" @@ -205,6 +205,7 @@ func TestUpdateUser(t *testing.T) { require.Nil(t, err) require.NotNil(t, u) require.Less(t, u.LastPictureUpdate, iLastPictureUpdate) + require.Empty(t, u.Password) }) t.Run("fails if profile picture is updated when user has custom profile picture and username is changed", func(t *testing.T) { @@ -1731,11 +1732,12 @@ func TestPatchUser(t *testing.T) { }) t.Run("Patch username with a new username", func(t *testing.T) { - _, err := th.App.PatchUser(th.Context, testUser.Id, &model.UserPatch{ + u, err := th.App.PatchUser(th.Context, testUser.Id, &model.UserPatch{ Username: model.NewString(model.NewId()), }, true) require.Nil(t, err) + require.Empty(t, u.Password) }) } @@ -1823,6 +1825,11 @@ func TestCreateUserWithInitialPreferences(t *testing.T) { assert.Equal(t, model.PreferenceRecommendedNextSteps, recommendedNextStepsPref[0].Category) assert.Equal(t, "hide", recommendedNextStepsPref[0].Name) assert.Equal(t, "false", recommendedNextStepsPref[0].Value) + + gmASdmNoticeViewedPref, appErr := th.App.GetPreferenceByCategoryAndNameForUser(testUser.Id, model.PreferenceCategorySystemNotice, "GMasDM") + require.Nil(t, appErr) + assert.Equal(t, "GMasDM", gmASdmNoticeViewedPref.Name) + assert.Equal(t, "true", gmASdmNoticeViewedPref.Value) }) t.Run("successfully create a guest user with initial tutorial and recommended steps preferences", func(t *testing.T) { @@ -1840,6 +1847,11 @@ func TestCreateUserWithInitialPreferences(t *testing.T) { assert.Equal(t, model.PreferenceRecommendedNextSteps, recommendedNextStepsPref[0].Category) assert.Equal(t, "hide", recommendedNextStepsPref[0].Name) assert.Equal(t, "false", recommendedNextStepsPref[0].Value) + + gmASdmNoticeViewedPref, appErr := th.App.GetPreferenceByCategoryAndNameForUser(testUser.Id, model.PreferenceCategorySystemNotice, "GMasDM") + require.Nil(t, appErr) + assert.Equal(t, "GMasDM", gmASdmNoticeViewedPref.Name) + assert.Equal(t, "true", gmASdmNoticeViewedPref.Value) }) } diff --git a/server/channels/app/users/helper_test.go b/server/channels/app/users/helper_test.go index 05f6ca4e691..0a6eff38308 100644 --- a/server/channels/app/users/helper_test.go +++ b/server/channels/app/users/helper_test.go @@ -11,7 +11,8 @@ import ( "testing" "github.com/mattermost/mattermost/server/public/model" - "github.com/mattermost/mattermost/server/v8/channels/app/request" + "github.com/mattermost/mattermost/server/public/shared/mlog" + "github.com/mattermost/mattermost/server/public/shared/request" "github.com/mattermost/mattermost/server/v8/channels/store" "github.com/mattermost/mattermost/server/v8/config" ) @@ -78,7 +79,7 @@ func setupTestHelper(s store.Store, includeCacheLayer bool, tb testing.TB) *Test oAuthStore: s.OAuth(), config: configStore.Get, }, - Context: request.EmptyContext(nil), + Context: request.EmptyContext(mlog.CreateConsoleTestLogger(tb)), configStore: configStore, dbStore: s, LogBuffer: buffer, diff --git a/server/channels/app/users/users.go b/server/channels/app/users/users.go index 1418ee251a0..85617d73720 100644 --- a/server/channels/app/users/users.go +++ b/server/channels/app/users/users.go @@ -105,6 +105,10 @@ func (us *UserService) GetUserByEmail(email string) (*model.User, error) { return us.store.GetByEmail(email) } +func (us *UserService) GetUserByRemoteID(remoteID string) (*model.User, error) { + return us.store.GetByRemoteID(remoteID) +} + func (us *UserService) GetUserByAuth(authData *string, authService string) (*model.User, error) { return us.store.GetByAuth(authData, authService) } diff --git a/server/channels/app/webhook.go b/server/channels/app/webhook.go index e5600ef3738..bca2b42bff8 100644 --- a/server/channels/app/webhook.go +++ b/server/channels/app/webhook.go @@ -16,7 +16,7 @@ import ( "github.com/mattermost/mattermost/server/public/model" "github.com/mattermost/mattermost/server/public/shared/mlog" - "github.com/mattermost/mattermost/server/v8/channels/app/request" + "github.com/mattermost/mattermost/server/public/shared/request" "github.com/mattermost/mattermost/server/v8/channels/store" "github.com/mattermost/mattermost/server/v8/channels/utils" ) diff --git a/server/channels/app/websocket_router.go b/server/channels/app/websocket_router.go deleted file mode 100644 index 218261132bc..00000000000 --- a/server/channels/app/websocket_router.go +++ /dev/null @@ -1,113 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package app - -// import ( -// "net/http" - -// "github.com/mattermost/mattermost/server/public/model" -// "github.com/mattermost/mattermost/server/public/shared/i18n" -// "github.com/mattermost/mattermost/server/public/shared/mlog" -// ) - -// type webSocketHandler interface { -// ServeWebSocket(*WebConn, *model.WebSocketRequest) -// } - -// type WebSocketRouter struct { -// handlers map[string]webSocketHandler -// } - -// func (wr *WebSocketRouter) Handle(action string, handler webSocketHandler) { -// wr.handlers[action] = handler -// } - -// func (wr *WebSocketRouter) ServeWebSocket(conn *WebConn, r *model.WebSocketRequest) { -// if r.Action == "" { -// err := model.NewAppError("ServeWebSocket", "api.web_socket_router.no_action.app_error", nil, "", http.StatusBadRequest) -// returnWebSocketError(conn.App, conn, r, err) -// return -// } - -// if r.Seq <= 0 { -// err := model.NewAppError("ServeWebSocket", "api.web_socket_router.bad_seq.app_error", nil, "", http.StatusBadRequest) -// returnWebSocketError(conn.App, conn, r, err) -// return -// } - -// if r.Action == model.WebsocketAuthenticationChallenge { -// if conn.GetSessionToken() != "" { -// return -// } - -// token, ok := r.Data["token"].(string) -// if !ok { -// conn.WebSocket.Close() -// return -// } - -// session, err := conn.App.GetSession(token) -// if err != nil { -// conn.WebSocket.Close() -// return -// } -// conn.SetSession(session) -// conn.SetSessionToken(session.Token) -// conn.UserId = session.UserId - -// conn.App.HubRegister(conn) - -// conn.App.Srv().Go(func() { -// conn.App.SetStatusOnline(session.UserId, false) -// conn.App.UpdateLastActivityAtIfNeeded(*session) -// }) - -// resp := model.NewWebSocketResponse(model.StatusOk, r.Seq, nil) -// hub := conn.App.GetHubForUserId(conn.UserId) -// if hub == nil { -// return -// } -// hub.SendMessage(conn, resp) - -// return -// } - -// if !conn.IsAuthenticated() { -// err := model.NewAppError("ServeWebSocket", "api.web_socket_router.not_authenticated.app_error", nil, "", http.StatusUnauthorized) -// returnWebSocketError(conn.App, conn, r, err) -// return -// } - -// handler, ok := wr.handlers[r.Action] -// if !ok { -// err := model.NewAppError("ServeWebSocket", "api.web_socket_router.bad_action.app_error", nil, "", http.StatusInternalServerError) -// returnWebSocketError(conn.App, conn, r, err) -// return -// } - -// handler.ServeWebSocket(conn, r) -// } - -// func returnWebSocketError(app *App, conn *WebConn, r *model.WebSocketRequest, err *model.AppError) { -// logF := mlog.Error -// if err.StatusCode >= http.StatusBadRequest && err.StatusCode < http.StatusInternalServerError { -// logF = mlog.Debug -// } -// logF( -// "websocket routing error.", -// mlog.Int64("seq", r.Seq), -// mlog.String("user_id", conn.UserId), -// mlog.String("system_message", err.SystemMessage(i18n.T)), -// mlog.Err(err), -// ) - -// hub := app.GetHubForUserId(conn.UserId) -// if hub == nil { -// return -// } - -// err.DetailedError = "" -// errorResp := model.NewWebSocketError(r.Seq, err) -// hub.SendMessage(conn, errorResp) -// } diff --git a/server/channels/audit/audit_test.go b/server/channels/audit/audit_test.go index db48b12c099..a488eee84ab 100644 --- a/server/channels/audit/audit_test.go +++ b/server/channels/audit/audit_test.go @@ -32,7 +32,7 @@ func TestAudit_LogRecord(t *testing.T) { audit.LogRecord(mlog.LvlAuditAPI, rec) }, []string{ - `{"timestamp":0,"level":"audit-api","msg":"","event_name":"","status":"","actor":{"user_id":"","session_id":"","client":"","ip_address":""},"event":{"parameters":null,"prior_state":null,"resulting_state":null,"object_type":""},"meta":null,"error":{}}`, + `{"timestamp":0,"level":"audit-api","msg":"","event_name":"","status":"","actor":{"user_id":"","session_id":"","client":"","ip_address":"","x_forwarded_for":""},"event":{"parameters":null,"prior_state":null,"resulting_state":null,"object_type":""},"meta":null,"error":{}}`, }, }, { @@ -56,7 +56,7 @@ func TestAudit_LogRecord(t *testing.T) { audit.LogRecord(mlog.LvlAuditAPI, rec) }, []string{ - strings.Replace(`{"timestamp":0,"level":"audit-api","msg":"","event_name":"User.Update","status":"success","actor":{"user_id":"","session_id":"","client":"","ip_address":""},"event":{"parameters":null,"prior_state":{"allow_marketing":false,"auth_service":"","bot_description":"","bot_last_icon_update":0,"create_at":0,"delete_at":0,"disable_welcome_email":false,"email":"","email_verified":false,"failed_attempts":0,"id":"_____USERID_____","is_bot":false,"last_activity_at":0,"last_password_update":0,"last_picture_update":0,"locale":"","mfa_active":false,"notify_props":null,"position":"","props":null,"remote_id":null,"roles":"","terms_of_service_create_at":0,"terms_of_service_id":"","timezone":null,"update_at":0,"username":"TestABC"},"resulting_state":{"allow_marketing":false,"auth_service":"","bot_description":"","bot_last_icon_update":0,"create_at":0,"delete_at":0,"disable_welcome_email":false,"email":"","email_verified":false,"failed_attempts":0,"id":"_____USERID_____","is_bot":false,"last_activity_at":0,"last_password_update":0,"last_picture_update":0,"locale":"","mfa_active":false,"notify_props":null,"position":"","props":null,"remote_id":null,"roles":"","terms_of_service_create_at":0,"terms_of_service_id":"","timezone":null,"update_at":0,"username":"TestDEF"},"object_type":"user"},"meta":null,"error":{}}`, "_____USERID_____", userId, -1), + strings.Replace(`{"timestamp":0,"level":"audit-api","msg":"","event_name":"User.Update","status":"success","actor":{"user_id":"","session_id":"","client":"","ip_address":"","x_forwarded_for":""},"event":{"parameters":null,"prior_state":{"allow_marketing":false,"auth_service":"","bot_description":"","bot_last_icon_update":0,"create_at":0,"delete_at":0,"disable_welcome_email":false,"email":"","email_verified":false,"failed_attempts":0,"id":"_____USERID_____","is_bot":false,"last_activity_at":0,"last_password_update":0,"last_picture_update":0,"locale":"","mfa_active":false,"notify_props":null,"position":"","props":null,"remote_id":null,"roles":"","terms_of_service_create_at":0,"terms_of_service_id":"","timezone":null,"update_at":0,"username":"TestABC"},"resulting_state":{"allow_marketing":false,"auth_service":"","bot_description":"","bot_last_icon_update":0,"create_at":0,"delete_at":0,"disable_welcome_email":false,"email":"","email_verified":false,"failed_attempts":0,"id":"_____USERID_____","is_bot":false,"last_activity_at":0,"last_password_update":0,"last_picture_update":0,"locale":"","mfa_active":false,"notify_props":null,"position":"","props":null,"remote_id":null,"roles":"","terms_of_service_create_at":0,"terms_of_service_id":"","timezone":null,"update_at":0,"username":"TestDEF"},"object_type":"user"},"meta":null,"error":{}}`, "_____USERID_____", userId, -1), }, }, } diff --git a/server/channels/audit/record.go b/server/channels/audit/record.go index 2e12b1d7cb9..fd516bfc507 100644 --- a/server/channels/audit/record.go +++ b/server/channels/audit/record.go @@ -23,10 +23,11 @@ type EventData struct { // EventActor is the subject triggering the event type EventActor struct { - UserId string `json:"user_id"` - SessionId string `json:"session_id"` - Client string `json:"client"` - IpAddress string `json:"ip_address"` + UserId string `json:"user_id"` + SessionId string `json:"session_id"` + Client string `json:"client"` + IpAddress string `json:"ip_address"` + XForwardedFor string `json:"x_forwarded_for"` } // EventMeta is a key-value store to store related information to the event that is not directly related to the modified entity diff --git a/server/channels/db/migrations/migrations.list b/server/channels/db/migrations/migrations.list index 5e73369a56c..559c7d0a266 100644 --- a/server/channels/db/migrations/migrations.list +++ b/server/channels/db/migrations/migrations.list @@ -218,8 +218,12 @@ channels/db/migrations/mysql/000108_remove_orphaned_oauth_preferences.down.sql channels/db/migrations/mysql/000108_remove_orphaned_oauth_preferences.up.sql channels/db/migrations/mysql/000109_create_persistent_notifications.down.sql channels/db/migrations/mysql/000109_create_persistent_notifications.up.sql -channels/db/migrations/mysql/000110_update_vacuuming.down.sql -channels/db/migrations/mysql/000110_update_vacuuming.up.sql +channels/db/migrations/mysql/000111_update_vacuuming.down.sql +channels/db/migrations/mysql/000111_update_vacuuming.up.sql +channels/db/migrations/mysql/000112_rework_desktop_tokens.down.sql +channels/db/migrations/mysql/000112_rework_desktop_tokens.up.sql +channels/db/migrations/mysql/000113_create_retentionidsfordeletion_table.down.sql +channels/db/migrations/mysql/000113_create_retentionidsfordeletion_table.up.sql channels/db/migrations/postgres/000001_create_teams.down.sql channels/db/migrations/postgres/000001_create_teams.up.sql channels/db/migrations/postgres/000002_create_team_members.down.sql @@ -438,5 +442,9 @@ channels/db/migrations/postgres/000108_remove_orphaned_oauth_preferences.down.sq channels/db/migrations/postgres/000108_remove_orphaned_oauth_preferences.up.sql channels/db/migrations/postgres/000109_create_persistent_notifications.down.sql channels/db/migrations/postgres/000109_create_persistent_notifications.up.sql -channels/db/migrations/postgres/000110_update_vacuuming.down.sql -channels/db/migrations/postgres/000110_update_vacuuming.up.sql +channels/db/migrations/postgres/000111_update_vacuuming.down.sql +channels/db/migrations/postgres/000111_update_vacuuming.up.sql +channels/db/migrations/postgres/000112_rework_desktop_tokens.down.sql +channels/db/migrations/postgres/000112_rework_desktop_tokens.up.sql +channels/db/migrations/postgres/000113_create_retentionidsfordeletion_table.down.sql +channels/db/migrations/postgres/000113_create_retentionidsfordeletion_table.up.sql diff --git a/server/channels/db/migrations/mysql/000110_update_vacuuming.down.sql b/server/channels/db/migrations/mysql/000111_update_vacuuming.down.sql similarity index 100% rename from server/channels/db/migrations/mysql/000110_update_vacuuming.down.sql rename to server/channels/db/migrations/mysql/000111_update_vacuuming.down.sql diff --git a/server/channels/db/migrations/mysql/000110_update_vacuuming.up.sql b/server/channels/db/migrations/mysql/000111_update_vacuuming.up.sql similarity index 100% rename from server/channels/db/migrations/mysql/000110_update_vacuuming.up.sql rename to server/channels/db/migrations/mysql/000111_update_vacuuming.up.sql diff --git a/server/channels/db/migrations/mysql/000112_rework_desktop_tokens.down.sql b/server/channels/db/migrations/mysql/000112_rework_desktop_tokens.down.sql new file mode 100644 index 00000000000..a0067811600 --- /dev/null +++ b/server/channels/db/migrations/mysql/000112_rework_desktop_tokens.down.sql @@ -0,0 +1,16 @@ +SET @preparedStatement = (SELECT IF( + ( + SELECT COUNT(*) FROM INFORMATION_SCHEMA.STATISTICS + WHERE table_name = 'DesktopTokens' + AND table_schema = DATABASE() + AND index_name = 'idx_desktoptokens_createat' + ) > 0, + 'DROP INDEX idx_desktoptokens_createat ON DesktopTokens;', + 'SELECT 1' +)); + +PREPARE removeIndexIfExists FROM @preparedStatement; +EXECUTE removeIndexIfExists; +DEALLOCATE PREPARE removeIndexIfExists; + +DROP TABLE IF EXISTS DesktopTokens; \ No newline at end of file diff --git a/server/channels/db/migrations/mysql/000112_rework_desktop_tokens.up.sql b/server/channels/db/migrations/mysql/000112_rework_desktop_tokens.up.sql new file mode 100644 index 00000000000..64c1ec7ef84 --- /dev/null +++ b/server/channels/db/migrations/mysql/000112_rework_desktop_tokens.up.sql @@ -0,0 +1,38 @@ +SET @preparedStatement = (SELECT IF( + ( + SELECT COUNT(*) FROM INFORMATION_SCHEMA.STATISTICS + WHERE table_name = 'DesktopTokens' + AND table_schema = DATABASE() + AND index_name = 'idx_desktoptokens_createat' + ) > 0, + 'DROP INDEX idx_desktoptokens_createat ON DesktopTokens;', + 'SELECT 1' +)); + +PREPARE removeIndexIfExists FROM @preparedStatement; +EXECUTE removeIndexIfExists; +DEALLOCATE PREPARE removeIndexIfExists; + +DROP TABLE IF EXISTS DesktopTokens; + +CREATE TABLE IF NOT EXISTS DesktopTokens ( + Token varchar(64) NOT NULL, + CreateAt bigint NOT NULL, + UserId varchar(26) NOT NULL, + PRIMARY KEY (Token) +); + +SET @preparedStatement = (SELECT IF( + ( + SELECT COUNT(*) FROM INFORMATION_SCHEMA.STATISTICS + WHERE table_name = 'DesktopTokens' + AND table_schema = DATABASE() + AND index_name = 'idx_desktoptokens_token_createat' + ) > 0, + 'SELECT 1', + 'CREATE INDEX idx_desktoptokens_token_createat ON DesktopTokens(Token, CreateAt);' +)); + +PREPARE createIndexIfNotExists FROM @preparedStatement; +EXECUTE createIndexIfNotExists; +DEALLOCATE PREPARE createIndexIfNotExists; \ No newline at end of file diff --git a/server/channels/db/migrations/mysql/000113_create_retentionidsfordeletion_table.down.sql b/server/channels/db/migrations/mysql/000113_create_retentionidsfordeletion_table.down.sql new file mode 100644 index 00000000000..d3a3dccf27f --- /dev/null +++ b/server/channels/db/migrations/mysql/000113_create_retentionidsfordeletion_table.down.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS RetentionIdsForDeletion; diff --git a/server/channels/db/migrations/mysql/000113_create_retentionidsfordeletion_table.up.sql b/server/channels/db/migrations/mysql/000113_create_retentionidsfordeletion_table.up.sql new file mode 100644 index 00000000000..cba69c2ff42 --- /dev/null +++ b/server/channels/db/migrations/mysql/000113_create_retentionidsfordeletion_table.up.sql @@ -0,0 +1,7 @@ +CREATE TABLE IF NOT EXISTS RetentionIdsForDeletion ( + Id varchar(26) NOT NULL, + TableName varchar(64), + Ids json, + PRIMARY KEY (Id), + KEY idx_retentionidsfordeletion_tablename (TableName) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; diff --git a/server/channels/db/migrations/postgres/000110_update_vacuuming.down.sql b/server/channels/db/migrations/postgres/000111_update_vacuuming.down.sql similarity index 100% rename from server/channels/db/migrations/postgres/000110_update_vacuuming.down.sql rename to server/channels/db/migrations/postgres/000111_update_vacuuming.down.sql diff --git a/server/channels/db/migrations/postgres/000110_update_vacuuming.up.sql b/server/channels/db/migrations/postgres/000111_update_vacuuming.up.sql similarity index 100% rename from server/channels/db/migrations/postgres/000110_update_vacuuming.up.sql rename to server/channels/db/migrations/postgres/000111_update_vacuuming.up.sql diff --git a/server/channels/db/migrations/postgres/000112_rework_desktop_tokens.down.sql b/server/channels/db/migrations/postgres/000112_rework_desktop_tokens.down.sql new file mode 100644 index 00000000000..9564f85d555 --- /dev/null +++ b/server/channels/db/migrations/postgres/000112_rework_desktop_tokens.down.sql @@ -0,0 +1,2 @@ +DROP INDEX IF EXISTS idx_desktoptokens_token_createat; +DROP TABLE IF EXISTS desktoptokens; diff --git a/server/channels/db/migrations/postgres/000112_rework_desktop_tokens.up.sql b/server/channels/db/migrations/postgres/000112_rework_desktop_tokens.up.sql new file mode 100644 index 00000000000..4442555cbdc --- /dev/null +++ b/server/channels/db/migrations/postgres/000112_rework_desktop_tokens.up.sql @@ -0,0 +1,11 @@ +DROP INDEX IF EXISTS idx_desktoptokens_createat; +DROP TABLE IF EXISTS desktoptokens; + +CREATE TABLE IF NOT EXISTS desktoptokens ( + token VARCHAR(64) NOT NULL, + createat BIGINT NOT NULL, + userid VARCHAR(26) NOT NULL, + PRIMARY KEY (token) +); + +CREATE INDEX IF NOT EXISTS idx_desktoptokens_token_createat ON desktoptokens(token, createat) diff --git a/server/channels/db/migrations/postgres/000113_create_retentionidsfordeletion_table.down.sql b/server/channels/db/migrations/postgres/000113_create_retentionidsfordeletion_table.down.sql new file mode 100644 index 00000000000..60af609bd55 --- /dev/null +++ b/server/channels/db/migrations/postgres/000113_create_retentionidsfordeletion_table.down.sql @@ -0,0 +1,3 @@ +DROP INDEX IF EXISTS idx_retentionidsfordeletion_tablename; + +DROP TABLE IF EXISTS retentionidsfordeletion; diff --git a/server/channels/db/migrations/postgres/000113_create_retentionidsfordeletion_table.up.sql b/server/channels/db/migrations/postgres/000113_create_retentionidsfordeletion_table.up.sql new file mode 100644 index 00000000000..c4457bfd76c --- /dev/null +++ b/server/channels/db/migrations/postgres/000113_create_retentionidsfordeletion_table.up.sql @@ -0,0 +1,7 @@ +CREATE TABLE IF NOT EXISTS retentionidsfordeletion ( + id varchar(26) PRIMARY KEY, + tablename varchar(64), + ids varchar(26)[] +); + +CREATE INDEX IF NOT EXISTS idx_retentionidsfordeletion_tablename ON retentionidsfordeletion (tablename); diff --git a/server/channels/jobs/active_users/scheduler.go b/server/channels/jobs/active_users/scheduler.go index 42dc1675721..3543923dee3 100644 --- a/server/channels/jobs/active_users/scheduler.go +++ b/server/channels/jobs/active_users/scheduler.go @@ -12,7 +12,7 @@ import ( const schedFreq = 10 * time.Minute -func MakeScheduler(jobServer *jobs.JobServer) model.Scheduler { +func MakeScheduler(jobServer *jobs.JobServer) *jobs.PeriodicScheduler { isEnabled := func(cfg *model.Config) bool { return *cfg.MetricsSettings.Enable } diff --git a/server/channels/jobs/active_users/worker.go b/server/channels/jobs/active_users/worker.go index 08e235fdcd6..a7239eaa2b1 100644 --- a/server/channels/jobs/active_users/worker.go +++ b/server/channels/jobs/active_users/worker.go @@ -10,11 +10,9 @@ import ( "github.com/mattermost/mattermost/server/v8/einterfaces" ) -const ( - JobName = "ActiveUsers" -) +func MakeWorker(jobServer *jobs.JobServer, store store.Store, getMetrics func() einterfaces.MetricsInterface) *jobs.SimpleWorker { + const workerName = "ActiveUsers" -func MakeWorker(jobServer *jobs.JobServer, store store.Store, getMetrics func() einterfaces.MetricsInterface) model.Worker { isEnabled := func(cfg *model.Config) bool { return *cfg.MetricsSettings.Enable } @@ -31,6 +29,6 @@ func MakeWorker(jobServer *jobs.JobServer, store store.Store, getMetrics func() } return nil } - worker := jobs.NewSimpleWorker(JobName, jobServer, execute, isEnabled) + worker := jobs.NewSimpleWorker(workerName, jobServer, execute, isEnabled) return worker } diff --git a/server/channels/jobs/base_schedulers.go b/server/channels/jobs/base_schedulers.go index 881b840a2d5..c1cb5362b74 100644 --- a/server/channels/jobs/base_schedulers.go +++ b/server/channels/jobs/base_schedulers.go @@ -9,6 +9,7 @@ import ( "time" "github.com/mattermost/mattermost/server/public/model" + "github.com/mattermost/mattermost/server/public/shared/request" ) type PeriodicScheduler struct { @@ -18,6 +19,8 @@ type PeriodicScheduler struct { enabledFunc func(cfg *model.Config) bool } +var _ Scheduler = (*DailyScheduler)(nil) + func NewPeriodicScheduler(jobs *JobServer, jobType string, period time.Duration, enabledFunc func(cfg *model.Config) bool) *PeriodicScheduler { return &PeriodicScheduler{ period: period, @@ -36,8 +39,8 @@ func (scheduler *PeriodicScheduler) NextScheduleTime(_ *model.Config, _ time.Tim return &nextTime } -func (scheduler *PeriodicScheduler) ScheduleJob(_ *model.Config /* pendingJobs */, _ bool /* lastSuccessfulJob */, _ *model.Job) (*model.Job, *model.AppError) { - return scheduler.jobs.CreateJob(scheduler.jobType, nil) +func (scheduler *PeriodicScheduler) ScheduleJob(c *request.Context, _ *model.Config /* pendingJobs */, _ bool /* lastSuccessfulJob */, _ *model.Job) (*model.Job, *model.AppError) { + return scheduler.jobs.CreateJob(c, scheduler.jobType, nil) } type DailyScheduler struct { @@ -47,6 +50,8 @@ type DailyScheduler struct { enabledFunc func(cfg *model.Config) bool } +var _ Scheduler = (*DailyScheduler)(nil) + func NewDailyScheduler(jobs *JobServer, jobType string, startTimeFunc func(cfg *model.Config) *time.Time, enabledFunc func(cfg *model.Config) bool) *DailyScheduler { return &DailyScheduler{ startTimeFunc: startTimeFunc, @@ -69,8 +74,8 @@ func (scheduler *DailyScheduler) NextScheduleTime(cfg *model.Config, now time.Ti return GenerateNextStartDateTime(now, *scheduledTime) } -func (scheduler *DailyScheduler) ScheduleJob(_ *model.Config /* pendingJobs */, _ bool /* lastSuccessfulJob */, _ *model.Job) (*model.Job, *model.AppError) { - return scheduler.jobs.CreateJob(scheduler.jobType, nil) +func (scheduler *DailyScheduler) ScheduleJob(c *request.Context, _ *model.Config /* pendingJobs */, _ bool /* lastSuccessfulJob */, _ *model.Job) (*model.Job, *model.AppError) { + return scheduler.jobs.CreateJob(c, scheduler.jobType, nil) } const jitterRange = 2000 // milliseconds diff --git a/server/channels/jobs/base_workers.go b/server/channels/jobs/base_workers.go index c427a81ab27..e16b0317016 100644 --- a/server/channels/jobs/base_workers.go +++ b/server/channels/jobs/base_workers.go @@ -8,6 +8,7 @@ import ( "github.com/mattermost/mattermost/server/public/model" "github.com/mattermost/mattermost/server/public/shared/mlog" + "github.com/mattermost/mattermost/server/public/shared/request" ) type SimpleWorker struct { @@ -16,6 +17,7 @@ type SimpleWorker struct { stopped chan bool jobs chan model.Job jobServer *JobServer + logger mlog.LoggerIFace execute func(job *model.Job) error isEnabled func(cfg *model.Config) bool } @@ -27,6 +29,7 @@ func NewSimpleWorker(name string, jobServer *JobServer, execute func(job *model. stopped: make(chan bool, 1), jobs: make(chan model.Job), jobServer: jobServer, + logger: jobServer.Logger().With(mlog.String("workername", name)), execute: execute, isEnabled: isEnabled, } @@ -34,27 +37,29 @@ func NewSimpleWorker(name string, jobServer *JobServer, execute func(job *model. } func (worker *SimpleWorker) Run() { - mlog.Debug("Worker started", mlog.String("worker", worker.name)) + worker.logger.Debug("Worker started") defer func() { - mlog.Debug("Worker finished", mlog.String("worker", worker.name)) + worker.logger.Debug("Worker finished") worker.stopped <- true }() for { select { case <-worker.stop: - mlog.Debug("Worker received stop signal", mlog.String("worker", worker.name)) + worker.logger.Debug("Worker received stop signal") return case job := <-worker.jobs: - mlog.Debug("Worker received a new candidate job.", mlog.String("worker", worker.name)) + job.Logger = job.Logger.With(mlog.String("workername", worker.name)) + + job.Logger.Debug("Worker received a new candidate job") worker.DoJob(&job) } } } func (worker *SimpleWorker) Stop() { - mlog.Debug("Worker stopping", mlog.String("worker", worker.name)) + worker.logger.Debug("Worker stopping") worker.stop <- true <-worker.stopped } @@ -69,48 +74,48 @@ func (worker *SimpleWorker) IsEnabled(cfg *model.Config) bool { func (worker *SimpleWorker) DoJob(job *model.Job) { if claimed, err := worker.jobServer.ClaimJob(job); err != nil { - mlog.Warn("SimpleWorker experienced an error while trying to claim job", - mlog.String("worker", worker.name), - mlog.String("job_id", job.Id), - mlog.Err(err)) + job.Logger.Warn("SimpleWorker experienced an error while trying to claim job", mlog.Err(err)) return } else if !claimed { return } - var appErr *model.AppError + c := request.EmptyContext(worker.logger) + // We get the job again because ClaimJob changes the job status. - job, appErr = worker.jobServer.GetJob(job.Id) + newJob, appErr := worker.jobServer.GetJob(c, job.Id) if appErr != nil { - mlog.Error("SimpleWorker: job execution error", mlog.String("worker", worker.name), mlog.String("job_id", job.Id), mlog.Err(appErr)) + job.Logger.Error("SimpleWorker: job execution error", mlog.Err(appErr)) worker.setJobError(job, appErr) + return } + job = newJob err := worker.execute(job) if err != nil { - mlog.Error("SimpleWorker: job execution error", mlog.String("worker", worker.name), mlog.String("job_id", job.Id), mlog.Err(err)) + job.Logger.Error("SimpleWorker: job execution error", mlog.Err(err)) worker.setJobError(job, model.NewAppError("DoJob", "app.job.error", nil, "", http.StatusInternalServerError).Wrap(err)) return } - mlog.Info("SimpleWorker: Job is complete", mlog.String("worker", worker.name), mlog.String("job_id", job.Id)) + job.Logger.Info("SimpleWorker: Job is complete") worker.setJobSuccess(job) } func (worker *SimpleWorker) setJobSuccess(job *model.Job) { if err := worker.jobServer.SetJobProgress(job, 100); err != nil { - mlog.Error("Worker: Failed to update progress for job", mlog.String("worker", worker.name), mlog.String("job_id", job.Id), mlog.Err(err)) + job.Logger.Error("Worker: Failed to update progress for job", mlog.Err(err)) worker.setJobError(job, err) } if err := worker.jobServer.SetJobSuccess(job); err != nil { - mlog.Error("SimpleWorker: Failed to set success for job", mlog.String("worker", worker.name), mlog.String("job_id", job.Id), mlog.Err(err)) + job.Logger.Error("SimpleWorker: Failed to set success for job", mlog.Err(err)) worker.setJobError(job, err) } } func (worker *SimpleWorker) setJobError(job *model.Job, appError *model.AppError) { if err := worker.jobServer.SetJobError(job, appError); err != nil { - mlog.Error("SimpleWorker: Failed to set job error", mlog.String("worker", worker.name), mlog.String("job_id", job.Id), mlog.Err(err)) + job.Logger.Error("SimpleWorker: Failed to set job error", mlog.Err(err)) } } diff --git a/server/channels/jobs/base_workers_test.go b/server/channels/jobs/base_workers_test.go new file mode 100644 index 00000000000..56fc4cef595 --- /dev/null +++ b/server/channels/jobs/base_workers_test.go @@ -0,0 +1,43 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package jobs + +import ( + "errors" + "testing" + + "github.com/mattermost/mattermost/server/public/model" + "github.com/mattermost/mattermost/server/public/shared/mlog" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" +) + +func TestSimpleWorkerPanic(t *testing.T) { + jobServer, mockStore, mockMetrics := makeJobServer(t) + + job := &model.Job{ + Id: "job_id", + Type: "job_type", + Logger: jobServer.logger.(*mlog.Logger), + } + + exec := func(_ *model.Job) error { + return nil + } + + isEnabled := func(_ *model.Config) bool { + return true + } + + mockStore.JobStore.On("UpdateStatusOptimistically", "job_id", model.JobStatusPending, model.JobStatusInProgress).Return(true, nil) + mockStore.JobStore.On("UpdateOptimistically", mock.AnythingOfType("*model.Job"), model.JobStatusInProgress).Return(true, nil) + mockStore.JobStore.On("Get", mock.AnythingOfType("*request.Context"), "job_id").Return(nil, errors.New("test")) + mockMetrics.On("IncrementJobActive", "job_type") + mockMetrics.On("DecrementJobActive", "job_type") + sWorker := NewSimpleWorker("test", jobServer, exec, isEnabled) + + require.NotPanics(t, func() { + sWorker.DoJob(job) + }) +} diff --git a/server/channels/jobs/cleanup_desktop_tokens/scheduler.go b/server/channels/jobs/cleanup_desktop_tokens/scheduler.go new file mode 100644 index 00000000000..9770633cf1b --- /dev/null +++ b/server/channels/jobs/cleanup_desktop_tokens/scheduler.go @@ -0,0 +1,20 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package cleanup_desktop_tokens + +import ( + "time" + + "github.com/mattermost/mattermost/server/public/model" + "github.com/mattermost/mattermost/server/v8/channels/jobs" +) + +const schedFreq = 1 * time.Hour + +func MakeScheduler(jobServer *jobs.JobServer) *jobs.PeriodicScheduler { + isEnabled := func(cfg *model.Config) bool { + return true + } + return jobs.NewPeriodicScheduler(jobServer, model.JobTypeCleanupDesktopTokens, schedFreq, isEnabled) +} diff --git a/server/channels/jobs/cleanup_desktop_tokens/worker.go b/server/channels/jobs/cleanup_desktop_tokens/worker.go new file mode 100644 index 00000000000..69c7b42c0d0 --- /dev/null +++ b/server/channels/jobs/cleanup_desktop_tokens/worker.go @@ -0,0 +1,36 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package cleanup_desktop_tokens + +import ( + "time" + + "github.com/mattermost/mattermost/server/public/model" + "github.com/mattermost/mattermost/server/v8/channels/jobs" + "github.com/mattermost/mattermost/server/v8/channels/store" + "github.com/mattermost/mattermost/server/v8/platform/services/configservice" +) + +const jobName = "CleanupDesktopTokens" +const maxAge = 5 * time.Minute + +type AppIface interface { + configservice.ConfigService + ListDirectory(path string) ([]string, *model.AppError) + FileModTime(path string) (time.Time, *model.AppError) + RemoveFile(path string) *model.AppError +} + +func MakeWorker(jobServer *jobs.JobServer, store store.Store) *jobs.SimpleWorker { + isEnabled := func(cfg *model.Config) bool { + return true + } + execute := func(job *model.Job) error { + defer jobServer.HandleJobPanic(job) + + return store.DesktopTokens().DeleteOlderThan(time.Now().Add(-maxAge).Unix()) + } + worker := jobs.NewSimpleWorker(jobName, jobServer, execute, isEnabled) + return worker +} diff --git a/server/channels/jobs/expirynotify/scheduler.go b/server/channels/jobs/expirynotify/scheduler.go index d7e1c2c590c..9fbc4db027a 100644 --- a/server/channels/jobs/expirynotify/scheduler.go +++ b/server/channels/jobs/expirynotify/scheduler.go @@ -12,7 +12,7 @@ import ( const schedFreq = 10 * time.Minute -func MakeScheduler(jobServer *jobs.JobServer) model.Scheduler { +func MakeScheduler(jobServer *jobs.JobServer) *jobs.PeriodicScheduler { isEnabled := func(cfg *model.Config) bool { return *cfg.ServiceSettings.ExtendSessionLengthWithActivity } diff --git a/server/channels/jobs/expirynotify/worker.go b/server/channels/jobs/expirynotify/worker.go index c6b32679869..dc08f815940 100644 --- a/server/channels/jobs/expirynotify/worker.go +++ b/server/channels/jobs/expirynotify/worker.go @@ -8,11 +8,9 @@ import ( "github.com/mattermost/mattermost/server/v8/channels/jobs" ) -const ( - JobName = "ExpiryNotify" -) +func MakeWorker(jobServer *jobs.JobServer, notifySessionsExpired func() error) *jobs.SimpleWorker { + const workerName = "ExpiryNotify" -func MakeWorker(jobServer *jobs.JobServer, notifySessionsExpired func() error) model.Worker { isEnabled := func(cfg *model.Config) bool { return *cfg.ServiceSettings.ExtendSessionLengthWithActivity } @@ -21,5 +19,5 @@ func MakeWorker(jobServer *jobs.JobServer, notifySessionsExpired func() error) m return notifySessionsExpired() } - return jobs.NewSimpleWorker(JobName, jobServer, execute, isEnabled) + return jobs.NewSimpleWorker(workerName, jobServer, execute, isEnabled) } diff --git a/server/channels/jobs/export_delete/scheduler.go b/server/channels/jobs/export_delete/scheduler.go index 87469e90d78..e7c1232872e 100644 --- a/server/channels/jobs/export_delete/scheduler.go +++ b/server/channels/jobs/export_delete/scheduler.go @@ -12,7 +12,7 @@ import ( const schedFreq = 24 * time.Hour -func MakeScheduler(jobServer *jobs.JobServer) model.Scheduler { +func MakeScheduler(jobServer *jobs.JobServer) *jobs.PeriodicScheduler { isEnabled := func(cfg *model.Config) bool { return *cfg.ExportSettings.Directory != "" && *cfg.ExportSettings.RetentionDays > 0 } diff --git a/server/channels/jobs/export_delete/worker.go b/server/channels/jobs/export_delete/worker.go index 0974a4fba21..b1af9388493 100644 --- a/server/channels/jobs/export_delete/worker.go +++ b/server/channels/jobs/export_delete/worker.go @@ -15,8 +15,6 @@ import ( "github.com/mattermost/mattermost/server/v8/platform/services/configservice" ) -const jobName = "ExportDelete" - type AppIface interface { configservice.ConfigService ListExportDirectory(path string) ([]string, *model.AppError) @@ -24,7 +22,9 @@ type AppIface interface { RemoveExportFile(path string) *model.AppError } -func MakeWorker(jobServer *jobs.JobServer, app AppIface) model.Worker { +func MakeWorker(jobServer *jobs.JobServer, app AppIface) *jobs.SimpleWorker { + const workerName = "ExportDelete" + isEnabled := func(cfg *model.Config) bool { return *cfg.ExportSettings.Directory != "" && *cfg.ExportSettings.RetentionDays > 0 } @@ -43,7 +43,7 @@ func MakeWorker(jobServer *jobs.JobServer, app AppIface) model.Worker { filename := filepath.Base(exports[i]) modTime, appErr := app.ExportFileModTime(filepath.Join(exportPath, filename)) if appErr != nil { - mlog.Debug("Worker: Failed to get file modification time", + job.Logger.Debug("Worker: Failed to get file modification time", mlog.Err(appErr), mlog.String("export", exports[i])) errors.Append(appErr) continue @@ -52,7 +52,7 @@ func MakeWorker(jobServer *jobs.JobServer, app AppIface) model.Worker { if time.Now().After(modTime.Add(retentionTime)) { // remove file data from storage. if appErr := app.RemoveExportFile(exports[i]); appErr != nil { - mlog.Debug("Worker: Failed to remove file", + job.Logger.Debug("Worker: Failed to remove file", mlog.Err(appErr), mlog.String("export", exports[i])) errors.Append(appErr) continue @@ -61,10 +61,10 @@ func MakeWorker(jobServer *jobs.JobServer, app AppIface) model.Worker { } if err := errors.ErrorOrNil(); err != nil { - mlog.Warn("Worker: errors occurred", mlog.String("job-name", jobName), mlog.Err(err)) + job.Logger.Warn("Worker: errors occurred", mlog.Err(err)) } return nil } - worker := jobs.NewSimpleWorker(jobName, jobServer, execute, isEnabled) + worker := jobs.NewSimpleWorker(workerName, jobServer, execute, isEnabled) return worker } diff --git a/server/channels/jobs/export_process/worker.go b/server/channels/jobs/export_process/worker.go index 15dadc621f0..0d0ab28ee8c 100644 --- a/server/channels/jobs/export_process/worker.go +++ b/server/channels/jobs/export_process/worker.go @@ -10,13 +10,11 @@ import ( "github.com/mattermost/mattermost/server/public/model" "github.com/mattermost/mattermost/server/public/shared/mlog" - "github.com/mattermost/mattermost/server/v8/channels/app/request" + "github.com/mattermost/mattermost/server/public/shared/request" "github.com/mattermost/mattermost/server/v8/channels/jobs" "github.com/mattermost/mattermost/server/v8/platform/services/configservice" ) -const jobName = "ExportProcess" - type AppIface interface { configservice.ConfigService WriteExportFileContext(ctx context.Context, fr io.Reader, path string) (int64, *model.AppError) @@ -24,7 +22,9 @@ type AppIface interface { Log() *mlog.Logger } -func MakeWorker(jobServer *jobs.JobServer, app AppIface) model.Worker { +func MakeWorker(jobServer *jobs.JobServer, app AppIface) *jobs.SimpleWorker { + const workerName = "ExportProcess" + isEnabled := func(cfg *model.Config) bool { return true } execute := func(job *model.Job) error { defer jobServer.HandleJobPanic(job) @@ -38,6 +38,11 @@ func MakeWorker(jobServer *jobs.JobServer, app AppIface) model.Worker { opts.IncludeAttachments = true } + includeArchivedChannels, ok := job.Data["include_archived_channels"] + if ok && includeArchivedChannels == "true" { + opts.IncludeArchivedChannels = true + } + outPath := *app.Config().ExportSettings.Directory exportFilename := job.Id + "_export.zip" @@ -54,8 +59,7 @@ func MakeWorker(jobServer *jobs.JobServer, app AppIface) model.Worker { } }() - logger := app.Log().With(mlog.String("job_id", job.Id)) - appErr := app.BulkExport(request.EmptyContext(logger), wr, outPath, job, opts) + appErr := app.BulkExport(request.EmptyContext(job.Logger), wr, outPath, job, opts) wr.Close() // Close never returns an error if appErr != nil { @@ -64,6 +68,6 @@ func MakeWorker(jobServer *jobs.JobServer, app AppIface) model.Worker { return nil } - worker := jobs.NewSimpleWorker(jobName, jobServer, execute, isEnabled) + worker := jobs.NewSimpleWorker(workerName, jobServer, execute, isEnabled) return worker } diff --git a/server/channels/jobs/extract_content/worker.go b/server/channels/jobs/extract_content/worker.go index 7a6d6e182d9..7e18eafc542 100644 --- a/server/channels/jobs/extract_content/worker.go +++ b/server/channels/jobs/extract_content/worker.go @@ -19,13 +19,13 @@ var ignoredFiles = map[string]bool{ "mkv": true, } -const jobName = "ExtractContent" - type AppIface interface { ExtractContentFromFileInfo(fileInfo *model.FileInfo) error } -func MakeWorker(jobServer *jobs.JobServer, app AppIface, store store.Store) model.Worker { +func MakeWorker(jobServer *jobs.JobServer, app AppIface, store store.Store) *jobs.SimpleWorker { + const workerName = "ExtractContent" + isEnabled := func(cfg *model.Config) bool { return true } @@ -65,10 +65,10 @@ func MakeWorker(jobServer *jobs.JobServer, app AppIface, store store.Store) mode } for _, fileInfo := range fileInfos { if !ignoredFiles[fileInfo.Extension] { - mlog.Debug("extracting file", mlog.String("filename", fileInfo.Name), mlog.String("filepath", fileInfo.Path)) + job.Logger.Debug("Extracting file", mlog.String("filename", fileInfo.Name), mlog.String("filepath", fileInfo.Path)) err = app.ExtractContentFromFileInfo(fileInfo) if err != nil { - mlog.Warn("Failed to extract file content", mlog.Err(err), mlog.String("file_info_id", fileInfo.Id)) + job.Logger.Warn("Failed to extract file content", mlog.Err(err), mlog.String("file_info_id", fileInfo.Id)) nErrs++ } nFiles++ @@ -85,10 +85,10 @@ func MakeWorker(jobServer *jobs.JobServer, app AppIface, store store.Store) mode job.Data["processed"] = strconv.Itoa(nFiles) if err := jobServer.UpdateInProgressJobData(job); err != nil { - mlog.Error("Worker: Failed to update job data", mlog.String("worker", model.JobTypeExtractContent), mlog.String("job_id", job.Id), mlog.Err(err)) + job.Logger.Error("Worker: Failed to update job data", mlog.Err(err)) } return nil } - worker := jobs.NewSimpleWorker(jobName, jobServer, execute, isEnabled) + worker := jobs.NewSimpleWorker(workerName, jobServer, execute, isEnabled) return worker } diff --git a/server/channels/jobs/hosted_purchase_screening/scheduler.go b/server/channels/jobs/hosted_purchase_screening/scheduler.go index 5db6e7adf89..817abc7454c 100644 --- a/server/channels/jobs/hosted_purchase_screening/scheduler.go +++ b/server/channels/jobs/hosted_purchase_screening/scheduler.go @@ -12,7 +12,7 @@ import ( const schedFreq = 24 * time.Hour -func MakeScheduler(jobServer *jobs.JobServer, license *model.License) model.Scheduler { +func MakeScheduler(jobServer *jobs.JobServer, license *model.License) *jobs.PeriodicScheduler { isEnabled := func(cfg *model.Config) bool { return model.BuildEnterpriseReady == "true" && license == nil } diff --git a/server/channels/jobs/hosted_purchase_screening/worker.go b/server/channels/jobs/hosted_purchase_screening/worker.go index 0fc15d37e8b..368e94ea15c 100644 --- a/server/channels/jobs/hosted_purchase_screening/worker.go +++ b/server/channels/jobs/hosted_purchase_screening/worker.go @@ -12,7 +12,6 @@ import ( ) const ( - JobName = "HostedPurchaseScreening" // 3 days matches the expecation given in portal purchase flow. waitForScreeningDuration = 3 * 24 * time.Hour ) @@ -22,7 +21,9 @@ type ScreenTimeStore interface { PermanentDeleteByName(name string) (*model.System, error) } -func MakeWorker(jobServer *jobs.JobServer, license *model.License, screenTimeStore ScreenTimeStore) model.Worker { +func MakeWorker(jobServer *jobs.JobServer, license *model.License, screenTimeStore ScreenTimeStore) *jobs.SimpleWorker { + const workerName = "HostedPurchaseScreening" + isEnabled := func(_ *model.Config) bool { return !license.IsCloud() } @@ -44,6 +45,6 @@ func MakeWorker(jobServer *jobs.JobServer, license *model.License, screenTimeSto } return nil } - worker := jobs.NewSimpleWorker(JobName, jobServer, execute, isEnabled) + worker := jobs.NewSimpleWorker(workerName, jobServer, execute, isEnabled) return worker } diff --git a/server/channels/jobs/import_delete/scheduler.go b/server/channels/jobs/import_delete/scheduler.go index 2ef6ea9a889..ea65f34274d 100644 --- a/server/channels/jobs/import_delete/scheduler.go +++ b/server/channels/jobs/import_delete/scheduler.go @@ -12,7 +12,7 @@ import ( const schedFreq = 24 * time.Hour -func MakeScheduler(jobServer *jobs.JobServer) model.Scheduler { +func MakeScheduler(jobServer *jobs.JobServer) *jobs.PeriodicScheduler { isEnabled := func(cfg *model.Config) bool { return *cfg.ImportSettings.Directory != "" && *cfg.ImportSettings.RetentionDays > 0 } diff --git a/server/channels/jobs/import_delete/worker.go b/server/channels/jobs/import_delete/worker.go index 680a85a4c98..6554ff3131f 100644 --- a/server/channels/jobs/import_delete/worker.go +++ b/server/channels/jobs/import_delete/worker.go @@ -17,8 +17,6 @@ import ( "github.com/mattermost/mattermost/server/v8/platform/services/configservice" ) -const jobName = "ImportDelete" - type AppIface interface { configservice.ConfigService ListDirectory(path string) ([]string, *model.AppError) @@ -26,7 +24,9 @@ type AppIface interface { RemoveFile(path string) *model.AppError } -func MakeWorker(jobServer *jobs.JobServer, app AppIface, s store.Store) model.Worker { +func MakeWorker(jobServer *jobs.JobServer, app AppIface, s store.Store) *jobs.SimpleWorker { + const workerName = "ImportDelete" + isEnabled := func(cfg *model.Config) bool { return *cfg.ImportSettings.Directory != "" && *cfg.ImportSettings.RetentionDays > 0 } @@ -45,7 +45,7 @@ func MakeWorker(jobServer *jobs.JobServer, app AppIface, s store.Store) model.Wo filename := filepath.Base(imports[i]) modTime, appErr := app.FileModTime(filepath.Join(importPath, filename)) if appErr != nil { - mlog.Debug("Worker: Failed to get file modification time", + job.Logger.Debug("Worker: Failed to get file modification time", mlog.Err(appErr), mlog.String("import", imports[i])) multipleErrors.Append(appErr) continue @@ -60,7 +60,7 @@ func MakeWorker(jobServer *jobs.JobServer, app AppIface, s store.Store) model.Wo if len(filename) > minLen && filepath.Ext(filename) == model.IncompleteUploadSuffix { uploadID := filename[:26] if storeErr := s.UploadSession().Delete(uploadID); storeErr != nil { - mlog.Debug("Worker: Failed to delete UploadSession", + job.Logger.Debug("Worker: Failed to delete UploadSession", mlog.Err(storeErr), mlog.String("upload_id", uploadID)) multipleErrors.Append(storeErr) continue @@ -71,13 +71,13 @@ func MakeWorker(jobServer *jobs.JobServer, app AppIface, s store.Store) model.Wo info, storeErr := s.FileInfo().GetByPath(filePath) var nfErr *store.ErrNotFound if storeErr != nil && !errors.As(storeErr, &nfErr) { - mlog.Debug("Worker: Failed to get FileInfo", + job.Logger.Debug("Worker: Failed to get FileInfo", mlog.Err(storeErr), mlog.String("path", filePath)) multipleErrors.Append(storeErr) continue } else if storeErr == nil { if storeErr = s.FileInfo().PermanentDelete(info.Id); storeErr != nil { - mlog.Debug("Worker: Failed to delete FileInfo", + job.Logger.Debug("Worker: Failed to delete FileInfo", mlog.Err(storeErr), mlog.String("file_id", info.Id)) multipleErrors.Append(storeErr) continue @@ -87,7 +87,7 @@ func MakeWorker(jobServer *jobs.JobServer, app AppIface, s store.Store) model.Wo // remove file data from storage. if appErr := app.RemoveFile(imports[i]); appErr != nil { - mlog.Debug("Worker: Failed to remove file", + job.Logger.Debug("Worker: Failed to remove file", mlog.Err(appErr), mlog.String("import", imports[i])) multipleErrors.Append(appErr) continue @@ -96,10 +96,10 @@ func MakeWorker(jobServer *jobs.JobServer, app AppIface, s store.Store) model.Wo } if err := multipleErrors.ErrorOrNil(); err != nil { - mlog.Warn("Worker: errors occurred", mlog.String("job-name", jobName), mlog.Err(err)) + job.Logger.Warn("Worker: errors occurred", mlog.Err(err)) } return nil } - worker := jobs.NewSimpleWorker(jobName, jobServer, execute, isEnabled) + worker := jobs.NewSimpleWorker(workerName, jobServer, execute, isEnabled) return worker } diff --git a/server/channels/jobs/import_process/worker.go b/server/channels/jobs/import_process/worker.go index db07b4e5c55..a234751862f 100644 --- a/server/channels/jobs/import_process/worker.go +++ b/server/channels/jobs/import_process/worker.go @@ -14,14 +14,12 @@ import ( "github.com/mattermost/mattermost/server/public/model" "github.com/mattermost/mattermost/server/public/shared/mlog" - "github.com/mattermost/mattermost/server/v8/channels/app/request" + "github.com/mattermost/mattermost/server/public/shared/request" "github.com/mattermost/mattermost/server/v8/channels/jobs" "github.com/mattermost/mattermost/server/v8/platform/services/configservice" "github.com/mattermost/mattermost/server/v8/platform/shared/filestore" ) -const jobName = "ImportProcess" - type AppIface interface { configservice.ConfigService RemoveFile(path string) *model.AppError @@ -32,8 +30,10 @@ type AppIface interface { Log() *mlog.Logger } -func MakeWorker(jobServer *jobs.JobServer, app AppIface) model.Worker { - appContext := request.EmptyContext(app.Log()) +func MakeWorker(jobServer *jobs.JobServer, app AppIface) *jobs.SimpleWorker { + const workerName = "ImportProcess" + + appContext := request.EmptyContext(jobServer.Logger()) isEnabled := func(cfg *model.Config) bool { return true } @@ -113,6 +113,6 @@ func MakeWorker(jobServer *jobs.JobServer, app AppIface) model.Worker { } return nil } - worker := jobs.NewSimpleWorker(jobName, jobServer, execute, isEnabled) + worker := jobs.NewSimpleWorker(workerName, jobServer, execute, isEnabled) return worker } diff --git a/server/channels/jobs/jobs.go b/server/channels/jobs/jobs.go index f9f6c896004..3b67eee6636 100644 --- a/server/channels/jobs/jobs.go +++ b/server/channels/jobs/jobs.go @@ -4,7 +4,6 @@ package jobs import ( - "context" "errors" "fmt" "net/http" @@ -14,6 +13,7 @@ import ( "github.com/mattermost/mattermost/server/public/model" "github.com/mattermost/mattermost/server/public/shared/mlog" + "github.com/mattermost/mattermost/server/public/shared/request" "github.com/mattermost/mattermost/server/v8/channels/store" ) @@ -21,8 +21,8 @@ const ( CancelWatcherPollingInterval = 5000 ) -func (srv *JobServer) CreateJob(jobType string, jobData map[string]string) (*model.Job, *model.AppError) { - job, appErr := srv._createJob(jobType, jobData) +func (srv *JobServer) CreateJob(c *request.Context, jobType string, jobData map[string]string) (*model.Job, *model.AppError) { + job, appErr := srv._createJob(c, jobType, jobData) if appErr != nil { return nil, appErr } @@ -34,8 +34,8 @@ func (srv *JobServer) CreateJob(jobType string, jobData map[string]string) (*mod return job, nil } -func (srv *JobServer) CreateJobOnce(jobType string, jobData map[string]string) (*model.Job, *model.AppError) { - job, appErr := srv._createJob(jobType, jobData) +func (srv *JobServer) CreateJobOnce(c *request.Context, jobType string, jobData map[string]string) (*model.Job, *model.AppError) { + job, appErr := srv._createJob(c, jobType, jobData) if appErr != nil { return nil, appErr } @@ -47,7 +47,7 @@ func (srv *JobServer) CreateJobOnce(jobType string, jobData map[string]string) ( return job, nil } -func (srv *JobServer) _createJob(jobType string, jobData map[string]string) (*model.Job, *model.AppError) { +func (srv *JobServer) _createJob(c *request.Context, jobType string, jobData map[string]string) (*model.Job, *model.AppError) { job := model.Job{ Id: model.NewId(), Type: jobType, @@ -56,6 +56,8 @@ func (srv *JobServer) _createJob(jobType string, jobData map[string]string) (*mo Data: jobData, } + job.InitLogger(c.Logger()) + if err := job.IsValid(); err != nil { return nil, err } @@ -67,8 +69,8 @@ func (srv *JobServer) _createJob(jobType string, jobData map[string]string) (*mo return &job, nil } -func (srv *JobServer) GetJob(id string) (*model.Job, *model.AppError) { - job, err := srv.Store.Job().Get(id) +func (srv *JobServer) GetJob(c *request.Context, id string) (*model.Job, *model.AppError) { + job, err := srv.Store.Job().Get(c, id) if err != nil { var nfErr *store.ErrNotFound switch { @@ -212,9 +214,15 @@ func (srv *JobServer) HandleJobPanic(job *model.Job) { return } + var logger mlog.LoggerIFace = job.Logger + if job.Logger == nil { + // Fall back to JobServer logger + logger = srv.logger + } + sb := &strings.Builder{} pprof.Lookup("goroutine").WriteTo(sb, 2) - mlog.Error("Unhandled panic in job", mlog.Any("panic", r), mlog.Any("job", job), mlog.String("stack", sb.String())) + logger.Error("Unhandled panic in job", mlog.Any("panic", r), mlog.Any("job", job), mlog.String("stack", sb.String())) rerr, ok := r.(error) if !ok { @@ -223,20 +231,20 @@ func (srv *JobServer) HandleJobPanic(job *model.Job) { appErr := srv.SetJobError(job, model.NewAppError("HandleJobPanic", "app.job.update.app_error", nil, "", http.StatusInternalServerError)).Wrap(rerr) if appErr != nil { - mlog.Error("Failed to set the job status to 'failed'", mlog.Err(appErr), mlog.Any("job", job)) + logger.Error("Failed to set the job status to 'failed'", mlog.Err(appErr), mlog.Any("job", job)) } panic(r) } -func (srv *JobServer) RequestCancellation(jobId string) *model.AppError { +func (srv *JobServer) RequestCancellation(c *request.Context, jobId string) *model.AppError { updated, err := srv.Store.Job().UpdateStatusOptimistically(jobId, model.JobStatusPending, model.JobStatusCanceled) if err != nil { return model.NewAppError("RequestCancellation", "app.job.update.app_error", nil, "", http.StatusInternalServerError).Wrap(err) } if updated { if srv.metrics != nil { - job, err := srv.GetJob(jobId) + job, err := srv.GetJob(c, jobId) if err != nil { return model.NewAppError("RequestCancellation", "app.job.update.app_error", nil, "", http.StatusInternalServerError).Wrap(err) } @@ -259,17 +267,17 @@ func (srv *JobServer) RequestCancellation(jobId string) *model.AppError { return model.NewAppError("RequestCancellation", "jobs.request_cancellation.status.error", nil, "id="+jobId, http.StatusInternalServerError) } -func (srv *JobServer) CancellationWatcher(ctx context.Context, jobId string, cancelChan chan struct{}) { +func (srv *JobServer) CancellationWatcher(c *request.Context, jobId string, cancelChan chan struct{}) { for { select { - case <-ctx.Done(): - mlog.Debug("CancellationWatcher for Job Aborting as job has finished.", mlog.String("job_id", jobId)) + case <-c.Context().Done(): + c.Logger().Debug("CancellationWatcher for Job Aborting as job has finished.", mlog.String("job_id", jobId)) return case <-time.After(CancelWatcherPollingInterval * time.Millisecond): - mlog.Debug("CancellationWatcher for Job started polling.", mlog.String("job_id", jobId)) - jobStatus, err := srv.Store.Job().Get(jobId) + c.Logger().Debug("CancellationWatcher for Job started polling.", mlog.String("job_id", jobId)) + jobStatus, err := srv.Store.Job().Get(c, jobId) if err != nil { - mlog.Warn("Error getting job", mlog.String("job_id", jobId), mlog.Err(err)) + c.Logger().Warn("Error getting job", mlog.String("job_id", jobId), mlog.Err(err)) continue } if jobStatus.Status == model.JobStatusCancelRequested { @@ -298,8 +306,8 @@ func (srv *JobServer) CheckForPendingJobsByType(jobType string) (bool, *model.Ap return count > 0, nil } -func (srv *JobServer) GetJobsByTypeAndStatus(jobType string, status string) ([]*model.Job, *model.AppError) { - jobs, err := srv.Store.Job().GetAllByTypeAndStatus(jobType, status) +func (srv *JobServer) GetJobsByTypeAndStatus(c *request.Context, jobType string, status string) ([]*model.Job, *model.AppError) { + jobs, err := srv.Store.Job().GetAllByTypeAndStatus(c, jobType, status) if err != nil { return nil, model.NewAppError("GetJobsByTypeAndStatus", "app.job.get_all_jobs_by_type_and_status.app_error", nil, "", http.StatusInternalServerError).Wrap(err) } diff --git a/server/channels/jobs/jobs_test.go b/server/channels/jobs/jobs_test.go index a27208fb2fa..8bc4621ddea 100644 --- a/server/channels/jobs/jobs_test.go +++ b/server/channels/jobs/jobs_test.go @@ -9,9 +9,12 @@ import ( "net/http" "testing" + "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" "github.com/mattermost/mattermost/server/public/model" + "github.com/mattermost/mattermost/server/public/shared/mlog" + "github.com/mattermost/mattermost/server/public/shared/request" "github.com/mattermost/mattermost/server/v8/channels/store" "github.com/mattermost/mattermost/server/v8/channels/store/storetest" "github.com/mattermost/mattermost/server/v8/channels/utils/testutils" @@ -35,6 +38,7 @@ func makeJobServer(t *testing.T) (*JobServer, *storetest.Store, *mocks.MetricsIn ConfigService: configService, Store: mockStore, metrics: mockMetrics, + logger: mlog.CreateConsoleTestLogger(t), } return jobServer, mockStore, mockMetrics @@ -54,7 +58,7 @@ func makeTeamEditionJobServer(t *testing.T) (*JobServer, *storetest.Store) { mockStore.AssertExpectations(t) }) - jobServer := NewJobServer(configService, mockStore, nil) + jobServer := NewJobServer(configService, mockStore, nil, mlog.CreateConsoleTestLogger(t)) return jobServer, mockStore } @@ -498,11 +502,13 @@ func TestUpdateInProgressJobData(t *testing.T) { func TestHandleJobPanic(t *testing.T) { t.Run("no panic", func(t *testing.T) { jobServer, _, _ := makeJobServer(t) + logger := mlog.CreateConsoleTestLogger(t) job := &model.Job{ Type: model.JobTypeImportProcess, Status: model.JobStatusInProgress, } + job.InitLogger(logger) f := func() { defer jobServer.HandleJobPanic(job) @@ -515,11 +521,13 @@ func TestHandleJobPanic(t *testing.T) { t.Run("with panic string", func(t *testing.T) { jobServer, mockStore, metrics := makeJobServer(t) + logger := mlog.CreateConsoleTestLogger(t) job := &model.Job{ Type: model.JobTypeImportProcess, Status: model.JobStatusInProgress, } + job.InitLogger(logger) f := func() { defer jobServer.HandleJobPanic(job) @@ -535,11 +543,13 @@ func TestHandleJobPanic(t *testing.T) { t.Run("with panic error", func(t *testing.T) { jobServer, mockStore, metrics := makeJobServer(t) + logger := mlog.CreateConsoleTestLogger(t) job := &model.Job{ Type: model.JobTypeImportProcess, Status: model.JobStatusInProgress, } + job.InitLogger(logger) f := func() { defer jobServer.HandleJobPanic(job) @@ -555,12 +565,13 @@ func TestHandleJobPanic(t *testing.T) { } func TestRequestCancellation(t *testing.T) { + ctx := request.EmptyContext(mlog.CreateConsoleTestLogger(t)) t.Run("error cancelling", func(t *testing.T) { jobServer, mockStore, _ := makeJobServer(t) mockStore.JobStore.On("UpdateStatusOptimistically", "job_id", model.JobStatusPending, model.JobStatusCanceled).Return(false, &model.AppError{Message: "message"}) - err := jobServer.RequestCancellation("job_id") + err := jobServer.RequestCancellation(ctx, "job_id") expectErrorId(t, "app.job.update.app_error", err) }) @@ -568,9 +579,9 @@ func TestRequestCancellation(t *testing.T) { jobServer, mockStore, _ := makeJobServer(t) mockStore.JobStore.On("UpdateStatusOptimistically", "job_id", model.JobStatusPending, model.JobStatusCanceled).Return(true, nil) - mockStore.JobStore.On("Get", "job_id").Return(nil, &store.ErrNotFound{}) + mockStore.JobStore.On("Get", mock.AnythingOfType("*request.Context"), "job_id").Return(nil, &store.ErrNotFound{}) - err := jobServer.RequestCancellation("job_id") + err := jobServer.RequestCancellation(ctx, "job_id") expectErrorId(t, "app.job.update.app_error", err) }) @@ -583,10 +594,10 @@ func TestRequestCancellation(t *testing.T) { } mockStore.JobStore.On("UpdateStatusOptimistically", "job_id", model.JobStatusPending, model.JobStatusCanceled).Return(true, nil) - mockStore.JobStore.On("Get", "job_id").Return(job, nil) + mockStore.JobStore.On("Get", mock.AnythingOfType("*request.Context"), "job_id").Return(job, nil) mockMetrics.On("DecrementJobActive", "job_type") - err := jobServer.RequestCancellation("job_id") + err := jobServer.RequestCancellation(ctx, "job_id") require.Nil(t, err) }) @@ -595,7 +606,7 @@ func TestRequestCancellation(t *testing.T) { mockStore.JobStore.On("UpdateStatusOptimistically", "job_id", model.JobStatusPending, model.JobStatusCanceled).Return(true, nil) - err := jobServer.RequestCancellation("job_id") + err := jobServer.RequestCancellation(ctx, "job_id") require.Nil(t, err) }) @@ -605,7 +616,7 @@ func TestRequestCancellation(t *testing.T) { mockStore.JobStore.On("UpdateStatusOptimistically", "job_id", model.JobStatusPending, model.JobStatusCanceled).Return(false, nil) mockStore.JobStore.On("UpdateStatusOptimistically", "job_id", model.JobStatusInProgress, model.JobStatusCancelRequested).Return(false, &model.AppError{Message: "message"}) - err := jobServer.RequestCancellation("job_id") + err := jobServer.RequestCancellation(ctx, "job_id") expectErrorId(t, "app.job.update.app_error", err) }) @@ -615,7 +626,7 @@ func TestRequestCancellation(t *testing.T) { mockStore.JobStore.On("UpdateStatusOptimistically", "job_id", model.JobStatusPending, model.JobStatusCanceled).Return(false, nil) mockStore.JobStore.On("UpdateStatusOptimistically", "job_id", model.JobStatusInProgress, model.JobStatusCancelRequested).Return(true, nil) - err := jobServer.RequestCancellation("job_id") + err := jobServer.RequestCancellation(ctx, "job_id") require.Nil(t, err) }) @@ -625,7 +636,7 @@ func TestRequestCancellation(t *testing.T) { mockStore.JobStore.On("UpdateStatusOptimistically", "job_id", model.JobStatusPending, model.JobStatusCanceled).Return(false, nil) mockStore.JobStore.On("UpdateStatusOptimistically", "job_id", model.JobStatusInProgress, model.JobStatusCancelRequested).Return(false, nil) - err := jobServer.RequestCancellation("job_id") + err := jobServer.RequestCancellation(ctx, "job_id") expectErrorId(t, "jobs.request_cancellation.status.error", err) }) } diff --git a/server/channels/jobs/jobs_watcher.go b/server/channels/jobs/jobs_watcher.go index aea282ff83e..58b4dcf4325 100644 --- a/server/channels/jobs/jobs_watcher.go +++ b/server/channels/jobs/jobs_watcher.go @@ -9,6 +9,7 @@ import ( "github.com/mattermost/mattermost/server/public/model" "github.com/mattermost/mattermost/server/public/shared/mlog" + "github.com/mattermost/mattermost/server/public/shared/request" ) // Default polling interval for jobs termination. @@ -67,7 +68,7 @@ func (watcher *Watcher) Stop() { } func (watcher *Watcher) PollAndNotify() { - jobs, err := watcher.srv.Store.Job().GetAllByStatus(model.JobStatusPending) + jobs, err := watcher.srv.Store.Job().GetAllByStatus(request.EmptyContext(watcher.srv.logger), model.JobStatusPending) if err != nil { mlog.Error("Error occurred getting all pending statuses.", mlog.Err(err)) return diff --git a/server/channels/jobs/last_accessible_file/scheduler.go b/server/channels/jobs/last_accessible_file/scheduler.go index 8e2000e7a08..0e072cead5c 100644 --- a/server/channels/jobs/last_accessible_file/scheduler.go +++ b/server/channels/jobs/last_accessible_file/scheduler.go @@ -14,7 +14,7 @@ import ( const schedFreq = 2 * time.Hour -func MakeScheduler(jobServer *jobs.JobServer, license *model.License) model.Scheduler { +func MakeScheduler(jobServer *jobs.JobServer, license *model.License) *jobs.PeriodicScheduler { isEnabled := func(cfg *model.Config) bool { enabled := license != nil && *license.Features.Cloud mlog.Debug("Scheduler: isEnabled: "+strconv.FormatBool(enabled), mlog.String("scheduler", model.JobTypeLastAccessibleFile)) diff --git a/server/channels/jobs/last_accessible_file/worker.go b/server/channels/jobs/last_accessible_file/worker.go index 96616b952c6..67f9fc22f59 100644 --- a/server/channels/jobs/last_accessible_file/worker.go +++ b/server/channels/jobs/last_accessible_file/worker.go @@ -8,15 +8,13 @@ import ( "github.com/mattermost/mattermost/server/v8/channels/jobs" ) -const ( - JobName = "LastAccessibleFile" -) - type AppIface interface { ComputeLastAccessibleFileTime() error } -func MakeWorker(jobServer *jobs.JobServer, license *model.License, app AppIface) model.Worker { +func MakeWorker(jobServer *jobs.JobServer, license *model.License, app AppIface) *jobs.SimpleWorker { + const workerName = "LastAccessibleFile" + isEnabled := func(_ *model.Config) bool { return license != nil && *license.Features.Cloud } @@ -25,6 +23,6 @@ func MakeWorker(jobServer *jobs.JobServer, license *model.License, app AppIface) return app.ComputeLastAccessibleFileTime() } - worker := jobs.NewSimpleWorker(JobName, jobServer, execute, isEnabled) + worker := jobs.NewSimpleWorker(workerName, jobServer, execute, isEnabled) return worker } diff --git a/server/channels/jobs/last_accessible_post/scheduler.go b/server/channels/jobs/last_accessible_post/scheduler.go index e8192538879..710a778adba 100644 --- a/server/channels/jobs/last_accessible_post/scheduler.go +++ b/server/channels/jobs/last_accessible_post/scheduler.go @@ -14,7 +14,7 @@ import ( const schedFreq = 30 * time.Minute -func MakeScheduler(jobServer *jobs.JobServer, license *model.License) model.Scheduler { +func MakeScheduler(jobServer *jobs.JobServer, license *model.License) *jobs.PeriodicScheduler { isEnabled := func(cfg *model.Config) bool { enabled := license != nil && *license.Features.Cloud mlog.Debug("Scheduler: isEnabled: "+strconv.FormatBool(enabled), mlog.String("scheduler", model.JobTypeLastAccessiblePost)) diff --git a/server/channels/jobs/last_accessible_post/worker.go b/server/channels/jobs/last_accessible_post/worker.go index b4ee708bec8..0f57fb1a089 100644 --- a/server/channels/jobs/last_accessible_post/worker.go +++ b/server/channels/jobs/last_accessible_post/worker.go @@ -8,15 +8,13 @@ import ( "github.com/mattermost/mattermost/server/v8/channels/jobs" ) -const ( - JobName = "LastAccessiblePost" -) - type AppIface interface { ComputeLastAccessiblePostTime() error } -func MakeWorker(jobServer *jobs.JobServer, license *model.License, app AppIface) model.Worker { +func MakeWorker(jobServer *jobs.JobServer, license *model.License, app AppIface) *jobs.SimpleWorker { + const workerName = "LastAccessiblePost" + isEnabled := func(_ *model.Config) bool { return license != nil && license.Features != nil && *license.Features.Cloud } @@ -25,6 +23,6 @@ func MakeWorker(jobServer *jobs.JobServer, license *model.License, app AppIface) return app.ComputeLastAccessiblePostTime() } - worker := jobs.NewSimpleWorker(JobName, jobServer, execute, isEnabled) + worker := jobs.NewSimpleWorker(workerName, jobServer, execute, isEnabled) return worker } diff --git a/server/channels/jobs/migrations/helper_test.go b/server/channels/jobs/migrations/helper_test.go index 1cca9ae498c..f1a1fedb5d6 100644 --- a/server/channels/jobs/migrations/helper_test.go +++ b/server/channels/jobs/migrations/helper_test.go @@ -7,7 +7,10 @@ import ( "testing" "github.com/mattermost/mattermost/server/public/model" + "github.com/mattermost/mattermost/server/public/shared/mlog" + "github.com/mattermost/mattermost/server/public/shared/request" "github.com/mattermost/mattermost/server/v8/channels/store" + "github.com/stretchr/testify/require" ) func Setup(tb testing.TB) store.Store { @@ -16,17 +19,15 @@ func Setup(tb testing.TB) store.Store { return store } -func deleteAllJobsByTypeAndMigrationKey(store store.Store, jobType string, migrationKey string) { - jobs, err := store.Job().GetAllByType(model.JobTypeMigrations) - if err != nil { - panic(err) - } +func deleteAllJobsByTypeAndMigrationKey(t *testing.T, store store.Store, jobType string, migrationKey string) { + ctx := request.EmptyContext(mlog.CreateConsoleTestLogger(t)) + jobs, err := store.Job().GetAllByType(ctx, model.JobTypeMigrations) + require.NoError(t, err) for _, job := range jobs { if key, ok := job.Data[JobDataKeyMigration]; ok && key == migrationKey { - if _, err = store.Job().Delete(job.Id); err != nil { - panic(err) - } + _, err = store.Job().Delete(job.Id) + require.NoError(t, err) } } } diff --git a/server/channels/jobs/migrations/migrations.go b/server/channels/jobs/migrations/migrations.go index 28124bff734..87d5d6c5ab3 100644 --- a/server/channels/jobs/migrations/migrations.go +++ b/server/channels/jobs/migrations/migrations.go @@ -7,6 +7,7 @@ import ( "net/http" "github.com/mattermost/mattermost/server/public/model" + "github.com/mattermost/mattermost/server/public/shared/request" "github.com/mattermost/mattermost/server/v8/channels/store" ) @@ -25,12 +26,12 @@ func MakeMigrationsList() []string { } } -func GetMigrationState(migration string, store store.Store) (string, *model.Job, *model.AppError) { +func GetMigrationState(c *request.Context, migration string, store store.Store) (string, *model.Job, *model.AppError) { if _, err := store.System().GetByName(migration); err == nil { return MigrationStateCompleted, nil, nil } - jobs, err := store.Job().GetAllByType(model.JobTypeMigrations) + jobs, err := store.Job().GetAllByType(c, model.JobTypeMigrations) if err != nil { return "", nil, model.NewAppError("GetMigrationState", "app.job.get_all.app_error", nil, "", http.StatusInternalServerError).Wrap(err) } diff --git a/server/channels/jobs/migrations/migrations_test.go b/server/channels/jobs/migrations/migrations_test.go index 5b05c95b615..d30664369bd 100644 --- a/server/channels/jobs/migrations/migrations_test.go +++ b/server/channels/jobs/migrations/migrations_test.go @@ -10,6 +10,8 @@ import ( "github.com/stretchr/testify/require" "github.com/mattermost/mattermost/server/public/model" + "github.com/mattermost/mattermost/server/public/shared/mlog" + "github.com/mattermost/mattermost/server/public/shared/request" ) func TestGetMigrationState(t *testing.T) { @@ -17,13 +19,14 @@ func TestGetMigrationState(t *testing.T) { t.SkipNow() } store := Setup(t) + ctx := request.EmptyContext(mlog.CreateConsoleTestLogger(t)) migrationKey := model.NewId() - deleteAllJobsByTypeAndMigrationKey(store, model.JobTypeMigrations, migrationKey) + deleteAllJobsByTypeAndMigrationKey(t, store, model.JobTypeMigrations, migrationKey) // Test with no job yet. - state, job, err := GetMigrationState(migrationKey, store) + state, job, err := GetMigrationState(ctx, migrationKey, store) assert.Nil(t, err) assert.Nil(t, job) assert.Equal(t, "unscheduled", state) @@ -36,7 +39,7 @@ func TestGetMigrationState(t *testing.T) { nErr := store.System().Save(&system) assert.NoError(t, nErr) - state, job, err = GetMigrationState(migrationKey, store) + state, job, err = GetMigrationState(ctx, migrationKey, store) assert.Nil(t, err) assert.Nil(t, job) assert.Equal(t, "completed", state) @@ -58,7 +61,7 @@ func TestGetMigrationState(t *testing.T) { j1, nErr = store.Job().Save(j1) require.NoError(t, nErr) - state, job, err = GetMigrationState(migrationKey, store) + state, job, err = GetMigrationState(ctx, migrationKey, store) assert.Nil(t, err) assert.Equal(t, j1.Id, job.Id) assert.Equal(t, "in_progress", state) @@ -77,7 +80,7 @@ func TestGetMigrationState(t *testing.T) { j2, nErr = store.Job().Save(j2) require.NoError(t, nErr) - state, job, err = GetMigrationState(migrationKey, store) + state, job, err = GetMigrationState(ctx, migrationKey, store) assert.Nil(t, err) assert.Equal(t, j2.Id, job.Id) assert.Equal(t, "in_progress", state) @@ -96,7 +99,7 @@ func TestGetMigrationState(t *testing.T) { j3, nErr = store.Job().Save(j3) require.NoError(t, nErr) - state, job, err = GetMigrationState(migrationKey, store) + state, job, err = GetMigrationState(ctx, migrationKey, store) assert.Nil(t, err) assert.Equal(t, j3.Id, job.Id) assert.Equal(t, "unscheduled", state) diff --git a/server/channels/jobs/migrations/scheduler.go b/server/channels/jobs/migrations/scheduler.go index f8fb33d1a0c..2888c5f53a0 100644 --- a/server/channels/jobs/migrations/scheduler.go +++ b/server/channels/jobs/migrations/scheduler.go @@ -8,6 +8,7 @@ import ( "github.com/mattermost/mattermost/server/public/model" "github.com/mattermost/mattermost/server/public/shared/mlog" + "github.com/mattermost/mattermost/server/public/shared/request" "github.com/mattermost/mattermost/server/v8/channels/jobs" "github.com/mattermost/mattermost/server/v8/channels/store" ) @@ -22,7 +23,9 @@ type Scheduler struct { allMigrationsCompleted bool } -func MakeScheduler(jobServer *jobs.JobServer, store store.Store) model.Scheduler { +var _ jobs.Scheduler = (*Scheduler)(nil) + +func MakeScheduler(jobServer *jobs.JobServer, store store.Store) *Scheduler { return &Scheduler{jobServer, store, false} } @@ -41,27 +44,14 @@ func (scheduler *Scheduler) NextScheduleTime(cfg *model.Config, now time.Time, p } //nolint:unparam -func (scheduler *Scheduler) ScheduleJob(cfg *model.Config, pendingJobs bool, lastSuccessfulJob *model.Job) (*model.Job, *model.AppError) { - mlog.Debug("Scheduling Job", mlog.String("scheduler", model.JobTypeMigrations)) +func (scheduler *Scheduler) ScheduleJob(c *request.Context, cfg *model.Config, pendingJobs bool, lastSuccessfulJob *model.Job) (*model.Job, *model.AppError) { + c.Logger().Debug("Scheduling Job", mlog.String("scheduler", model.JobTypeMigrations)) // Work through the list of migrations in order. Schedule the first one that isn't done (assuming it isn't in progress already). for _, key := range MakeMigrationsList() { - state, job, err := GetMigrationState(key, scheduler.store) + state, job, err := GetMigrationState(c, key, scheduler.store) if err != nil { - mlog.Error("Failed to determine status of migration: ", mlog.String("scheduler", model.JobTypeMigrations), mlog.String("migration_key", key), mlog.Err(err)) - return nil, nil - } - - if state == MigrationStateInProgress { - // Check the migration job isn't wedged. - if job != nil && job.LastActivityAt < model.GetMillis()-MigrationJobWedgedTimeoutMilliseconds && job.CreateAt < model.GetMillis()-MigrationJobWedgedTimeoutMilliseconds { - mlog.Warn("Job appears to be wedged. Rescheduling another instance.", mlog.String("scheduler", model.JobTypeMigrations), mlog.String("wedged_job_id", job.Id), mlog.String("migration_key", key)) - if err := scheduler.jobServer.SetJobError(job, nil); err != nil { - mlog.Error("Worker: Failed to set job error", mlog.String("scheduler", model.JobTypeMigrations), mlog.String("job_id", job.Id), mlog.Err(err)) - } - return scheduler.createJob(key, job) - } - + c.Logger().Error("Failed to determine status of migration: ", mlog.String("scheduler", model.JobTypeMigrations), mlog.String("migration_key", key), mlog.Err(err)) return nil, nil } @@ -70,23 +60,41 @@ func (scheduler *Scheduler) ScheduleJob(cfg *model.Config, pendingJobs bool, las continue } - if state == MigrationStateUnscheduled { - mlog.Debug("Scheduling a new job for migration.", mlog.String("scheduler", model.JobTypeMigrations), mlog.String("migration_key", key)) - return scheduler.createJob(key, job) + if state == MigrationStateInProgress { + // Check the migration job isn't wedged. + if job != nil && job.LastActivityAt < model.GetMillis()-MigrationJobWedgedTimeoutMilliseconds && job.CreateAt < model.GetMillis()-MigrationJobWedgedTimeoutMilliseconds { + job.Logger.Warn("Job appears to be wedged. Rescheduling another instance.", mlog.String("scheduler", model.JobTypeMigrations), mlog.String("wedged_job_id", job.Id), mlog.String("migration_key", key)) + if err := scheduler.jobServer.SetJobError(job, nil); err != nil { + job.Logger.Error("Worker: Failed to set job error", mlog.String("scheduler", model.JobTypeMigrations), mlog.Err(err)) + } + return scheduler.createJob(c, key, job) + } + + return nil, nil } - mlog.Error("Unknown migration state. Not doing anything.", mlog.String("migration_state", state)) + if state == MigrationStateUnscheduled { + // GetMigrationState can return a nil job + logger := scheduler.jobServer.Logger() + if job != nil { + logger = job.Logger + } + logger.Debug("Scheduling a new job for migration.", mlog.String("scheduler", model.JobTypeMigrations), mlog.String("migration_key", key)) + return scheduler.createJob(c, key, job) + } + + job.Logger.Error("Unknown migration state. Not doing anything.", mlog.String("migration_state", state)) return nil, nil } // If we reached here, then there aren't any migrations left to run. scheduler.allMigrationsCompleted = true - mlog.Debug("All migrations are complete.", mlog.String("scheduler", model.JobTypeMigrations)) + c.Logger().Debug("All migrations are complete.", mlog.String("scheduler", model.JobTypeMigrations)) return nil, nil } -func (scheduler *Scheduler) createJob(migrationKey string, lastJob *model.Job) (*model.Job, *model.AppError) { +func (scheduler *Scheduler) createJob(c *request.Context, migrationKey string, lastJob *model.Job) (*model.Job, *model.AppError) { var lastDone string if lastJob != nil { lastDone = lastJob.Data[JobDataKeyMigrationLastDone] @@ -97,7 +105,7 @@ func (scheduler *Scheduler) createJob(migrationKey string, lastJob *model.Job) ( JobDataKeyMigrationLastDone: lastDone, } - job, err := scheduler.jobServer.CreateJob(model.JobTypeMigrations, data) + job, err := scheduler.jobServer.CreateJob(c, model.JobTypeMigrations, data) if err != nil { return nil, err } diff --git a/server/channels/jobs/migrations/worker.go b/server/channels/jobs/migrations/worker.go index bcbdc6e83c4..fbe7a8cc0ff 100644 --- a/server/channels/jobs/migrations/worker.go +++ b/server/channels/jobs/migrations/worker.go @@ -11,6 +11,7 @@ import ( "github.com/mattermost/mattermost/server/public/model" "github.com/mattermost/mattermost/server/public/shared/mlog" + "github.com/mattermost/mattermost/server/public/shared/request" "github.com/mattermost/mattermost/server/v8/channels/jobs" "github.com/mattermost/mattermost/server/v8/channels/store" ) @@ -25,17 +26,20 @@ type Worker struct { stopped chan bool jobs chan model.Job jobServer *jobs.JobServer + logger mlog.LoggerIFace store store.Store closed int32 } -func MakeWorker(jobServer *jobs.JobServer, store store.Store) model.Worker { +func MakeWorker(jobServer *jobs.JobServer, store store.Store) *Worker { + const workerName = "Migrations" worker := Worker{ - name: "Migrations", + name: workerName, stop: make(chan struct{}), stopped: make(chan bool, 1), jobs: make(chan model.Job), jobServer: jobServer, + logger: jobServer.Logger().With(mlog.String("workername", workerName)), store: store, } @@ -47,20 +51,22 @@ func (worker *Worker) Run() { if atomic.CompareAndSwapInt32(&worker.closed, 1, 0) { worker.stop = make(chan struct{}) } - mlog.Debug("Worker started", mlog.String("worker", worker.name)) + worker.logger.Debug("Worker started") defer func() { - mlog.Debug("Worker finished", mlog.String("worker", worker.name)) + worker.logger.Debug("Worker finished") worker.stopped <- true }() for { select { case <-worker.stop: - mlog.Debug("Worker received stop signal", mlog.String("worker", worker.name)) + worker.logger.Debug("Worker received stop signal") return case job := <-worker.jobs: - mlog.Debug("Worker received a new candidate job.", mlog.String("worker", worker.name)) + job.Logger = job.Logger.With(mlog.String("workername", worker.name)) + + job.Logger.Debug("Worker received a new candidate job") worker.DoJob(&job) } } @@ -71,7 +77,7 @@ func (worker *Worker) Stop() { if !atomic.CompareAndSwapInt32(&worker.closed, 0, 1) { return } - mlog.Debug("Worker stopping", mlog.String("worker", worker.name)) + worker.logger.Debug("Worker stopping") close(worker.stop) <-worker.stopped } @@ -88,47 +94,45 @@ func (worker *Worker) DoJob(job *model.Job) { defer worker.jobServer.HandleJobPanic(job) if claimed, err := worker.jobServer.ClaimJob(job); err != nil { - mlog.Info("Worker experienced an error while trying to claim job", - mlog.String("worker", worker.name), - mlog.String("job_id", job.Id), - mlog.String("error", err.Error())) + job.Logger.Info("Worker experienced an error while trying to claim job", mlog.Err(err)) return } else if !claimed { return } + cancelContext := request.EmptyContext(worker.logger) cancelCtx, cancelCancelWatcher := context.WithCancel(context.Background()) cancelWatcherChan := make(chan struct{}, 1) - go worker.jobServer.CancellationWatcher(cancelCtx, job.Id, cancelWatcherChan) - + cancelContext.SetContext(cancelCtx) + go worker.jobServer.CancellationWatcher(cancelContext, job.Id, cancelWatcherChan) defer cancelCancelWatcher() for { select { case <-cancelWatcherChan: - mlog.Debug("Worker: Job has been canceled via CancellationWatcher", mlog.String("worker", worker.name), mlog.String("job_id", job.Id)) + job.Logger.Debug("Worker: Job has been canceled via CancellationWatcher") worker.setJobCanceled(job) return case <-worker.stop: - mlog.Debug("Worker: Job has been canceled via Worker Stop", mlog.String("worker", worker.name), mlog.String("job_id", job.Id)) + job.Logger.Debug("Worker: Job has been canceled via Worker Stop") worker.setJobCanceled(job) return case <-time.After(TimeBetweenBatches * time.Millisecond): done, progress, err := worker.runMigration(job.Data[JobDataKeyMigration], job.Data[JobDataKeyMigrationLastDone]) if err != nil { - mlog.Error("Worker: Failed to run migration", mlog.String("worker", worker.name), mlog.String("job_id", job.Id), mlog.String("error", err.Error())) + job.Logger.Error("Worker: Failed to run migration", mlog.Err(err)) worker.setJobError(job, err) return } else if done { - mlog.Info("Worker: Job is complete", mlog.String("worker", worker.name), mlog.String("job_id", job.Id)) + job.Logger.Info("Worker: Job is complete") worker.setJobSuccess(job) return } else { job.Data[JobDataKeyMigrationLastDone] = progress if err := worker.jobServer.UpdateInProgressJobData(job); err != nil { - mlog.Error("Worker: Failed to update migration status data for job", mlog.String("worker", worker.name), mlog.String("job_id", job.Id), mlog.String("error", err.Error())) + job.Logger.Error("Worker: Failed to update migration status data for job", mlog.Err(err)) worker.setJobError(job, err) return } @@ -139,20 +143,20 @@ func (worker *Worker) DoJob(job *model.Job) { func (worker *Worker) setJobSuccess(job *model.Job) { if err := worker.jobServer.SetJobSuccess(job); err != nil { - mlog.Error("Worker: Failed to set success for job", mlog.String("worker", worker.name), mlog.String("job_id", job.Id), mlog.String("error", err.Error())) + job.Logger.Error("Worker: Failed to set success for job", mlog.Err(err)) worker.setJobError(job, err) } } func (worker *Worker) setJobError(job *model.Job, appError *model.AppError) { if err := worker.jobServer.SetJobError(job, appError); err != nil { - mlog.Error("Worker: Failed to set job error", mlog.String("worker", worker.name), mlog.String("job_id", job.Id), mlog.String("error", err.Error())) + job.Logger.Error("Worker: Failed to set job error", mlog.Err(err)) } } func (worker *Worker) setJobCanceled(job *model.Job) { if err := worker.jobServer.SetJobCanceled(job); err != nil { - mlog.Error("Worker: Failed to mark job as canceled", mlog.String("worker", worker.name), mlog.String("job_id", job.Id), mlog.String("error", err.Error())) + job.Logger.Error("Worker: Failed to mark job as canceled", mlog.Err(err)) } } diff --git a/server/channels/jobs/notify_admin/install_plugin_scheduler.go b/server/channels/jobs/notify_admin/install_plugin_scheduler.go index 55c88bc89d5..32c3af42037 100644 --- a/server/channels/jobs/notify_admin/install_plugin_scheduler.go +++ b/server/channels/jobs/notify_admin/install_plugin_scheduler.go @@ -14,7 +14,7 @@ import ( const installPluginSchedFreq = 24 * time.Hour -func MakeInstallPluginScheduler(jobServer *jobs.JobServer, license *model.License, jobType string) model.Scheduler { +func MakeInstallPluginScheduler(jobServer *jobs.JobServer, license *model.License, jobType string) *jobs.PeriodicScheduler { isEnabled := func(cfg *model.Config) bool { enabled := jobType == model.JobTypeInstallPluginNotifyAdmin mlog.Debug("Scheduler: isEnabled: "+strconv.FormatBool(enabled), mlog.String("scheduler", jobType)) diff --git a/server/channels/jobs/notify_admin/scheduler.go b/server/channels/jobs/notify_admin/scheduler.go index 3642ae5cbdc..30a7698e09a 100644 --- a/server/channels/jobs/notify_admin/scheduler.go +++ b/server/channels/jobs/notify_admin/scheduler.go @@ -14,7 +14,7 @@ import ( const schedFreq = 24 * time.Hour -func MakeScheduler(jobServer *jobs.JobServer, license *model.License, jobType string) model.Scheduler { +func MakeScheduler(jobServer *jobs.JobServer, license *model.License, jobType string) *jobs.PeriodicScheduler { isEnabled := func(cfg *model.Config) bool { enabled := license != nil && *license.Features.Cloud mlog.Debug("Scheduler: isEnabled: "+strconv.FormatBool(enabled), mlog.String("scheduler", jobType)) diff --git a/server/channels/jobs/notify_admin/worker.go b/server/channels/jobs/notify_admin/worker.go index d73024e7a6c..8d5d510c2ed 100644 --- a/server/channels/jobs/notify_admin/worker.go +++ b/server/channels/jobs/notify_admin/worker.go @@ -18,7 +18,7 @@ type AppIface interface { DoCheckForAdminNotifications(trial bool) *model.AppError } -func MakeUpgradeNotifyWorker(jobServer *jobs.JobServer, license *model.License, app AppIface) model.Worker { +func MakeUpgradeNotifyWorker(jobServer *jobs.JobServer, license *model.License, app AppIface) *jobs.SimpleWorker { isEnabled := func(_ *model.Config) bool { return license != nil && license.Features != nil && *license.Features.Cloud } @@ -36,7 +36,7 @@ func MakeUpgradeNotifyWorker(jobServer *jobs.JobServer, license *model.License, return worker } -func MakeTrialNotifyWorker(jobServer *jobs.JobServer, license *model.License, app AppIface) model.Worker { +func MakeTrialNotifyWorker(jobServer *jobs.JobServer, license *model.License, app AppIface) *jobs.SimpleWorker { isEnabled := func(_ *model.Config) bool { return license != nil && license.Features != nil && *license.Features.Cloud } @@ -54,7 +54,7 @@ func MakeTrialNotifyWorker(jobServer *jobs.JobServer, license *model.License, ap return worker } -func MakeInstallPluginNotifyWorker(jobServer *jobs.JobServer, app AppIface) model.Worker { +func MakeInstallPluginNotifyWorker(jobServer *jobs.JobServer, app AppIface) *jobs.SimpleWorker { isEnabled := func(_ *model.Config) bool { return true } diff --git a/server/channels/jobs/plugins/scheduler.go b/server/channels/jobs/plugins/scheduler.go index bb52cfb9882..8388f2a464a 100644 --- a/server/channels/jobs/plugins/scheduler.go +++ b/server/channels/jobs/plugins/scheduler.go @@ -12,7 +12,7 @@ import ( const schedFreq = 24 * time.Hour -func MakeScheduler(jobServer *jobs.JobServer) model.Scheduler { +func MakeScheduler(jobServer *jobs.JobServer) *jobs.PeriodicScheduler { isEnabled := func(cfg *model.Config) bool { return true } diff --git a/server/channels/jobs/plugins/worker.go b/server/channels/jobs/plugins/worker.go index 96fb46ef22b..34630fd2092 100644 --- a/server/channels/jobs/plugins/worker.go +++ b/server/channels/jobs/plugins/worker.go @@ -19,16 +19,19 @@ type Worker struct { stopped chan bool jobs chan model.Job jobServer *jobs.JobServer + logger mlog.LoggerIFace app AppIface } -func MakeWorker(jobServer *jobs.JobServer, app AppIface) model.Worker { +func MakeWorker(jobServer *jobs.JobServer, app AppIface) *Worker { + const workerName = "Plugins" worker := Worker{ - name: "Plugins", + name: workerName, stop: make(chan bool, 1), stopped: make(chan bool, 1), jobs: make(chan model.Job), jobServer: jobServer, + logger: jobServer.Logger().With(mlog.String("workername", workerName)), app: app, } @@ -36,27 +39,29 @@ func MakeWorker(jobServer *jobs.JobServer, app AppIface) model.Worker { } func (worker *Worker) Run() { - mlog.Debug("Worker started", mlog.String("worker", worker.name)) + worker.logger.Debug("Worker started") defer func() { - mlog.Debug("Worker finished", mlog.String("worker", worker.name)) + worker.logger.Debug("Worker finished") worker.stopped <- true }() for { select { case <-worker.stop: - mlog.Debug("Worker received stop signal", mlog.String("worker", worker.name)) + worker.logger.Debug("Worker received stop signal") return case job := <-worker.jobs: - mlog.Debug("Worker received a new candidate job.", mlog.String("worker", worker.name)) + job.Logger = job.Logger.With(mlog.String("workername", worker.name)) + + job.Logger.Debug("Worker received a new candidate job") worker.DoJob(&job) } } } func (worker *Worker) Stop() { - mlog.Debug("Worker stopping", mlog.String("worker", worker.name)) + worker.logger.Debug("Worker stopping") worker.stop <- true <-worker.stopped } @@ -71,34 +76,31 @@ func (worker *Worker) IsEnabled(cfg *model.Config) bool { func (worker *Worker) DoJob(job *model.Job) { if claimed, err := worker.jobServer.ClaimJob(job); err != nil { - mlog.Info("Worker experienced an error while trying to claim job", - mlog.String("worker", worker.name), - mlog.String("job_id", job.Id), - mlog.String("error", err.Error())) + job.Logger.Info("Worker experienced an error while trying to claim job", mlog.Err(err)) return } else if !claimed { return } if err := worker.app.DeleteAllExpiredPluginKeys(); err != nil { - mlog.Error("Worker: Failed to delete expired keys", mlog.String("worker", worker.name), mlog.String("job_id", job.Id), mlog.String("error", err.Error())) + job.Logger.Error("Worker: Failed to delete expired keys", mlog.Err(err)) worker.setJobError(job, err) return } - mlog.Info("Worker: Job is complete", mlog.String("worker", worker.name), mlog.String("job_id", job.Id)) + job.Logger.Info("Worker: Job is complete") worker.setJobSuccess(job) } func (worker *Worker) setJobSuccess(job *model.Job) { if err := worker.jobServer.SetJobSuccess(job); err != nil { - mlog.Error("Worker: Failed to set success for job", mlog.String("worker", worker.name), mlog.String("job_id", job.Id), mlog.String("error", err.Error())) + job.Logger.Error("Worker: Failed to set success for job", mlog.Err(err)) worker.setJobError(job, err) } } func (worker *Worker) setJobError(job *model.Job, appError *model.AppError) { if err := worker.jobServer.SetJobError(job, appError); err != nil { - mlog.Error("Worker: Failed to set job error", mlog.String("worker", worker.name), mlog.String("job_id", job.Id), mlog.String("error", err.Error())) + job.Logger.Error("Worker: Failed to set job error", mlog.Err(err)) } } diff --git a/server/channels/jobs/post_persistent_notifications/scheduler.go b/server/channels/jobs/post_persistent_notifications/scheduler.go index 0458a3e5e2c..43b1b19f845 100644 --- a/server/channels/jobs/post_persistent_notifications/scheduler.go +++ b/server/channels/jobs/post_persistent_notifications/scheduler.go @@ -19,7 +19,7 @@ func (scheduler *Scheduler) NextScheduleTime(cfg *model.Config, _ time.Time, _ b return &nextTime } -func MakeScheduler(jobServer *jobs.JobServer, licenseFunc func() *model.License) model.Scheduler { +func MakeScheduler(jobServer *jobs.JobServer, licenseFunc func() *model.License) *Scheduler { enabledFunc := func(_ *model.Config) bool { l := licenseFunc() return l != nil && (l.SkuShortName == model.LicenseShortSkuProfessional || l.SkuShortName == model.LicenseShortSkuEnterprise) diff --git a/server/channels/jobs/post_persistent_notifications/worker.go b/server/channels/jobs/post_persistent_notifications/worker.go index ca84b0f07a6..d5e279ca432 100644 --- a/server/channels/jobs/post_persistent_notifications/worker.go +++ b/server/channels/jobs/post_persistent_notifications/worker.go @@ -8,16 +8,14 @@ import ( "github.com/mattermost/mattermost/server/v8/channels/jobs" ) -const ( - JobName = "PostPersistentNotifications" -) - type AppIface interface { SendPersistentNotifications() error IsPersistentNotificationsEnabled() bool } -func MakeWorker(jobServer *jobs.JobServer, app AppIface) model.Worker { +func MakeWorker(jobServer *jobs.JobServer, app AppIface) *jobs.SimpleWorker { + const workerName = "PostPersistentNotifications" + isEnabled := func(_ *model.Config) bool { return app.IsPersistentNotificationsEnabled() } @@ -25,6 +23,6 @@ func MakeWorker(jobServer *jobs.JobServer, app AppIface) model.Worker { defer jobServer.HandleJobPanic(job) return app.SendPersistentNotifications() } - worker := jobs.NewSimpleWorker(JobName, jobServer, execute, isEnabled) + worker := jobs.NewSimpleWorker(workerName, jobServer, execute, isEnabled) return worker } diff --git a/server/channels/jobs/product_notices/scheduler.go b/server/channels/jobs/product_notices/scheduler.go index 77aa01e8c8c..f7160ded48d 100644 --- a/server/channels/jobs/product_notices/scheduler.go +++ b/server/channels/jobs/product_notices/scheduler.go @@ -19,7 +19,7 @@ func (scheduler *Scheduler) NextScheduleTime(cfg *model.Config, _ time.Time, pen return &nextTime } -func MakeScheduler(jobServer *jobs.JobServer) model.Scheduler { +func MakeScheduler(jobServer *jobs.JobServer) *Scheduler { isEnabled := func(cfg *model.Config) bool { return *cfg.AnnouncementSettings.AdminNoticesEnabled || *cfg.AnnouncementSettings.UserNoticesEnabled } diff --git a/server/channels/jobs/product_notices/worker.go b/server/channels/jobs/product_notices/worker.go index af1ae15a70d..c6a3379df15 100644 --- a/server/channels/jobs/product_notices/worker.go +++ b/server/channels/jobs/product_notices/worker.go @@ -9,13 +9,13 @@ import ( "github.com/mattermost/mattermost/server/v8/channels/jobs" ) -const jobName = "ProductNotices" - type AppIface interface { UpdateProductNotices() *model.AppError } -func MakeWorker(jobServer *jobs.JobServer, app AppIface) model.Worker { +func MakeWorker(jobServer *jobs.JobServer, app AppIface) *jobs.SimpleWorker { + const workerName = "ProductNotices" + isEnabled := func(cfg *model.Config) bool { return *cfg.AnnouncementSettings.AdminNoticesEnabled || *cfg.AnnouncementSettings.UserNoticesEnabled } @@ -23,11 +23,11 @@ func MakeWorker(jobServer *jobs.JobServer, app AppIface) model.Worker { defer jobServer.HandleJobPanic(job) if err := app.UpdateProductNotices(); err != nil { - mlog.Error("Worker: Failed to fetch product notices", mlog.String("worker", model.JobTypeProductNotices), mlog.String("job_id", job.Id), mlog.Err(err)) + job.Logger.Error("Worker: Failed to fetch product notices", mlog.Err(err)) return err } return nil } - worker := jobs.NewSimpleWorker(jobName, jobServer, execute, isEnabled) + worker := jobs.NewSimpleWorker(workerName, jobServer, execute, isEnabled) return worker } diff --git a/server/channels/jobs/resend_invitation_email/worker.go b/server/channels/jobs/resend_invitation_email/worker.go index 7946ea955a5..d87ce925b48 100644 --- a/server/channels/jobs/resend_invitation_email/worker.go +++ b/server/channels/jobs/resend_invitation_email/worker.go @@ -31,18 +31,21 @@ type ResendInvitationEmailWorker struct { stopped chan bool jobs chan model.Job jobServer *jobs.JobServer + logger mlog.LoggerIFace app AppIface store store.Store telemetryService *telemetry.TelemetryService } -func MakeWorker(jobServer *jobs.JobServer, app AppIface, store store.Store, telemetryService *telemetry.TelemetryService) model.Worker { +func MakeWorker(jobServer *jobs.JobServer, app AppIface, store store.Store, telemetryService *telemetry.TelemetryService) *ResendInvitationEmailWorker { + const workerName = "ResendInvitationEmail" worker := ResendInvitationEmailWorker{ - name: model.JobTypeResendInvitationEmail, + name: workerName, stop: make(chan bool, 1), stopped: make(chan bool, 1), jobs: make(chan model.Job), jobServer: jobServer, + logger: jobServer.Logger().With(mlog.String("workername", workerName)), app: app, store: store, telemetryService: telemetryService, @@ -51,20 +54,20 @@ func MakeWorker(jobServer *jobs.JobServer, app AppIface, store store.Store, tele } func (rseworker *ResendInvitationEmailWorker) Run() { - mlog.Debug("Worker started", mlog.String("worker", rseworker.name)) + rseworker.logger.Debug("Worker started") defer func() { - mlog.Debug("Worker finished", mlog.String("worker", rseworker.name)) + rseworker.logger.Debug("Worker finished") rseworker.stopped <- true }() for { select { case <-rseworker.stop: - mlog.Debug("Worker received stop signal", mlog.String("worker", rseworker.name)) + rseworker.logger.Debug("Worker received stop signal") return case job := <-rseworker.jobs: - mlog.Debug("Worker received a new candidate job.", mlog.String("worker", rseworker.name)) + job.Logger.Debug("Worker received a new candidate job") rseworker.DoJob(&job) } } @@ -75,7 +78,7 @@ func (rseworker *ResendInvitationEmailWorker) IsEnabled(cfg *model.Config) bool } func (rseworker *ResendInvitationEmailWorker) Stop() { - mlog.Debug("Worker stopping", mlog.String("worker", rseworker.name)) + rseworker.logger.Debug("Worker stopping") rseworker.stop <- true <-rseworker.stopped } @@ -96,14 +99,14 @@ func (rseworker *ResendInvitationEmailWorker) DoJob(job *model.Job) { func (rseworker *ResendInvitationEmailWorker) setJobSuccess(job *model.Job) { if err := rseworker.jobServer.SetJobSuccess(job); err != nil { - mlog.Error("Worker: Failed to set success for job", mlog.String("worker", rseworker.name), mlog.String("job_id", job.Id), mlog.String("error", err.Error())) + job.Logger.Error("Worker: Failed to set success for job", mlog.Err(err)) rseworker.setJobError(job, err) } } func (rseworker *ResendInvitationEmailWorker) setJobError(job *model.Job, appError *model.AppError) { if err := rseworker.jobServer.SetJobError(job, appError); err != nil { - mlog.Error("Worker: Failed to set job error", mlog.String("worker", rseworker.name), mlog.String("job_id", job.Id), mlog.String("error", err.Error())) + job.Logger.Error("Worker: Failed to set job error", mlog.Err(err)) } } @@ -179,14 +182,14 @@ func (rseworker *ResendInvitationEmailWorker) ResendEmails(job *model.Job, inter emailList, err := rseworker.cleanEmailData(emailListData) if err != nil { appErr := model.NewAppError("worker: "+rseworker.name, "job_id: "+job.Id, nil, "", http.StatusInternalServerError).Wrap(err) - mlog.Error("Worker: Failed to clean emails string data", mlog.String("worker", rseworker.name), mlog.String("job_id", job.Id), mlog.String("error", appErr.Error())) + job.Logger.Error("Worker: Failed to clean emails string data", mlog.Err(appErr)) rseworker.setJobError(job, appErr) } channelList, err := rseworker.cleanChannelsData(channelListData) if err != nil { appErr := model.NewAppError("worker: "+rseworker.name, "job_id: "+job.Id, nil, "", http.StatusInternalServerError).Wrap(err) - mlog.Error("Worker: Failed to clean channel string data", mlog.String("worker", rseworker.name), mlog.String("job_id", job.Id), mlog.String("error", appErr.Error())) + job.Logger.Error("Worker: Failed to clean channel string data", mlog.Err(appErr)) rseworker.setJobError(job, appErr) } @@ -202,7 +205,7 @@ func (rseworker *ResendInvitationEmailWorker) ResendEmails(job *model.Job, inter _, appErr := rseworker.app.InviteNewUsersToTeamGracefully(&memberInvite, teamID, job.Data["senderID"], interval) if appErr != nil { - mlog.Error("Worker: Failed to send emails", mlog.String("worker", rseworker.name), mlog.String("job_id", job.Id), mlog.String("error", appErr.Error())) + job.Logger.Error("Worker: Failed to send emails", mlog.Err(appErr)) rseworker.setJobError(job, appErr) } rseworker.telemetryService.SendTelemetry("track_invite_email_resend", map[string]any{interval: interval}) diff --git a/server/channels/jobs/s3_path_migration/s3_path_migration.go b/server/channels/jobs/s3_path_migration/s3_path_migration.go index 1157b7646e8..dcfde2012d4 100644 --- a/server/channels/jobs/s3_path_migration/s3_path_migration.go +++ b/server/channels/jobs/s3_path_migration/s3_path_migration.go @@ -12,20 +12,20 @@ import ( "github.com/mattermost/mattermost/server/public/model" "github.com/mattermost/mattermost/server/public/shared/mlog" + "github.com/mattermost/mattermost/server/public/shared/request" "github.com/mattermost/mattermost/server/v8/channels/jobs" "github.com/mattermost/mattermost/server/v8/channels/store" "github.com/mattermost/mattermost/server/v8/platform/shared/filestore" ) const ( - JobName = "S3PathMigration" - timeBetweenBatches = 1 * time.Second ) type S3PathMigrationWorker struct { name string jobServer *jobs.JobServer + logger mlog.LoggerIFace store store.Store fileBackend *filestore.S3FileBackend @@ -34,15 +34,17 @@ type S3PathMigrationWorker struct { jobs chan model.Job } -func MakeWorker(jobServer *jobs.JobServer, store store.Store, fileBackend filestore.FileBackend) model.Worker { +func MakeWorker(jobServer *jobs.JobServer, store store.Store, fileBackend filestore.FileBackend) *S3PathMigrationWorker { // If the type cast fails, it will be nil // which is checked later. s3Backend, _ := fileBackend.(*filestore.S3FileBackend) + const workerName = "S3PathMigration" worker := &S3PathMigrationWorker{ + name: workerName, jobServer: jobServer, + logger: jobServer.Logger().With(mlog.String("workername", workerName)), store: store, fileBackend: s3Backend, - name: JobName, stop: make(chan bool, 1), stopped: make(chan bool, 1), jobs: make(chan model.Job), @@ -51,27 +53,32 @@ func MakeWorker(jobServer *jobs.JobServer, store store.Store, fileBackend filest } func (worker *S3PathMigrationWorker) Run() { - mlog.Debug("Worker started", mlog.String("worker", worker.name)) + worker.logger.Debug("Worker started") + // We have to re-assign the stop channel again, because + // it might happen that the job was restarted due to a config change. + worker.stop = make(chan bool, 1) defer func() { - mlog.Debug("Worker finished", mlog.String("worker", worker.name)) + worker.logger.Debug("Worker finished") worker.stopped <- true }() for { select { case <-worker.stop: - mlog.Debug("Worker received stop signal", mlog.String("worker", worker.name)) + worker.logger.Debug("Worker received stop signal") return case job := <-worker.jobs: - mlog.Debug("Worker received a new candidate job.", mlog.String("worker", worker.name)) + job.Logger = job.Logger.With(mlog.String("workername", worker.name)) + + job.Logger.Debug("Worker received a new candidate job") worker.DoJob(&job) } } } func (worker *S3PathMigrationWorker) Stop() { - mlog.Debug("Worker stopping", mlog.String("worker", worker.name)) + worker.logger.Debug("Worker stopping") close(worker.stop) <-worker.stopped } @@ -101,10 +108,7 @@ func (worker *S3PathMigrationWorker) DoJob(job *model.Job) { defer worker.jobServer.HandleJobPanic(job) if claimed, err := worker.jobServer.ClaimJob(job); err != nil { - mlog.Warn("S3PathMigrationWorker experienced an error while trying to claim job", - mlog.String("worker", worker.name), - mlog.String("job_id", job.Id), - mlog.Err(err)) + job.Logger.Warn("S3PathMigrationWorker experienced an error while trying to claim job", mlog.Err(err)) return } else if !claimed { return @@ -112,16 +116,18 @@ func (worker *S3PathMigrationWorker) DoJob(job *model.Job) { if worker.fileBackend == nil { err := errors.New("no S3 file backend found") - mlog.Error("S3PathMigrationWorker: ", mlog.Err(err)) + job.Logger.Error("S3PathMigrationWorker: ", mlog.Err(err)) worker.setJobError(job, model.NewAppError("DoJob", model.NoTranslation, nil, "", http.StatusInternalServerError).Wrap(err)) return } + c := request.EmptyContext(worker.logger) + var appErr *model.AppError // We get the job again because ClaimJob changes the job status. - job, appErr = worker.jobServer.GetJob(job.Id) + job, appErr = worker.jobServer.GetJob(c, job.Id) if appErr != nil { - mlog.Error("S3PathMigrationWorker: job execution error", mlog.String("worker", worker.name), mlog.String("job_id", job.Id), mlog.Err(appErr)) + job.Logger.Error("S3PathMigrationWorker: job execution error", mlog.Err(appErr)) worker.setJobError(job, appErr) return } @@ -132,14 +138,14 @@ func (worker *S3PathMigrationWorker) DoJob(job *model.Job) { doneCount, appErr := worker.getJobMetadata(job, "done_file_count") if appErr != nil { - mlog.Error("S3PathMigrationWorker: failed to get done file count", mlog.String("worker", worker.name), mlog.String("job_id", job.Id), mlog.Err(appErr)) + job.Logger.Error("S3PathMigrationWorker: failed to get done file count", mlog.Err(appErr)) worker.setJobError(job, appErr) return } startTime, appErr := worker.getJobMetadata(job, "start_create_at") if appErr != nil { - mlog.Error("S3PathMigrationWorker: failed to get start create_at", mlog.String("worker", worker.name), mlog.String("job_id", job.Id), mlog.Err(appErr)) + job.Logger.Error("S3PathMigrationWorker: failed to get start create_at", mlog.Err(appErr)) worker.setJobError(job, appErr) return } @@ -155,14 +161,9 @@ func (worker *S3PathMigrationWorker) DoJob(job *model.Job) { for { select { case <-worker.stop: - mlog.Info("Worker: S3 Migration has been canceled via Worker Stop. Setting the job back to pending.", - mlog.String("workername", worker.name), - mlog.String("job_id", job.Id)) + job.Logger.Info("Worker: S3 Migration has been canceled via Worker Stop. Setting the job back to pending.") if err := worker.jobServer.SetJobPending(job); err != nil { - mlog.Error("Worker: Failed to mark job as pending", - mlog.String("workername", worker.name), - mlog.String("job_id", job.Id), - mlog.Err(err)) + worker.logger.Error("Worker: Failed to mark job as pending", mlog.Err(err)) } return case <-time.After(timeBetweenBatches): @@ -174,11 +175,11 @@ func (worker *S3PathMigrationWorker) DoJob(job *model.Job) { files, err = worker.store.FileInfo().GetFilesBatchForIndexing(int64(startTime), startFileID, true, pageSize) if err != nil { if tries > 3 { - mlog.Error("Worker: Failed to get files after multiple retries. Exiting") + job.Logger.Error("Worker: Failed to get files after multiple retries. Exiting") worker.setJobError(job, model.NewAppError("DoJob", model.NoTranslation, nil, "", http.StatusInternalServerError).Wrap(err)) return } - mlog.Warn("Failed to get file info for s3 migration. Retrying .. ", mlog.Err(err)) + job.Logger.Warn("Failed to get file info for s3 migration. Retrying .. ", mlog.Err(err)) // Wait a bit before trying again. time.Sleep(15 * time.Second) @@ -188,29 +189,29 @@ func (worker *S3PathMigrationWorker) DoJob(job *model.Job) { } if len(files) == 0 { - mlog.Info("S3PathMigrationWorker: Job is complete", mlog.String("worker", worker.name), mlog.String("job_id", job.Id)) + job.Logger.Info("S3PathMigrationWorker: Job is complete") worker.setJobSuccess(job) - worker.markAsComplete() + worker.markAsComplete(job) return } // Iterate through the rows in each page. for _, f := range files { - mlog.Debug("Processing file ID", mlog.String("id", f.Id), mlog.String("worker", worker.name), mlog.String("job_id", job.Id)) + job.Logger.Debug("Processing file ID", mlog.String("id", f.Id)) // We do not fail the job if a single image failed to encode. if f.Path != "" { if err := worker.fileBackend.DecodeFilePathIfNeeded(f.Path); err != nil { - mlog.Warn("Failed to encode S3 file path", mlog.String("path", f.Path), mlog.String("id", f.Id), mlog.Err(err)) + job.Logger.Warn("Failed to encode S3 file path", mlog.String("path", f.Path), mlog.String("id", f.Id), mlog.Err(err)) } } if f.PreviewPath != "" { if err := worker.fileBackend.DecodeFilePathIfNeeded(f.PreviewPath); err != nil { - mlog.Warn("Failed to encode S3 file path", mlog.String("path", f.PreviewPath), mlog.String("id", f.Id), mlog.Err(err)) + job.Logger.Warn("Failed to encode S3 file path", mlog.String("path", f.PreviewPath), mlog.String("id", f.Id), mlog.Err(err)) } } if f.ThumbnailPath != "" { if err := worker.fileBackend.DecodeFilePathIfNeeded(f.ThumbnailPath); err != nil { - mlog.Warn("Failed to encode S3 file path", mlog.String("path", f.ThumbnailPath), mlog.String("id", f.Id), mlog.Err(err)) + job.Logger.Warn("Failed to encode S3 file path", mlog.String("path", f.ThumbnailPath), mlog.String("id", f.Id), mlog.Err(err)) } } } @@ -231,7 +232,7 @@ func (worker *S3PathMigrationWorker) DoJob(job *model.Job) { } } -func (worker *S3PathMigrationWorker) markAsComplete() { +func (worker *S3PathMigrationWorker) markAsComplete(job *model.Job) { system := model.System{ Name: model.MigrationKeyS3Path, Value: "true", @@ -242,24 +243,24 @@ func (worker *S3PathMigrationWorker) markAsComplete() { // it will just fall through everything because all files would have // converted. The actual job is idempotent, so there won't be a problem. if err := worker.jobServer.Store.System().Save(&system); err != nil { - mlog.Error("Worker: Failed to mark s3 path migration as completed in the systems table.", mlog.String("workername", worker.name), mlog.Err(err)) + job.Logger.Error("Worker: Failed to mark s3 path migration as completed in the systems table.", mlog.Err(err)) } } func (worker *S3PathMigrationWorker) setJobSuccess(job *model.Job) { if err := worker.jobServer.SetJobProgress(job, 100); err != nil { - mlog.Error("Worker: Failed to update progress for job", mlog.String("worker", worker.name), mlog.String("job_id", job.Id), mlog.Err(err)) + job.Logger.Error("Worker: Failed to update progress for job", mlog.Err(err)) worker.setJobError(job, err) } if err := worker.jobServer.SetJobSuccess(job); err != nil { - mlog.Error("S3PathMigrationWorker: Failed to set success for job", mlog.String("worker", worker.name), mlog.String("job_id", job.Id), mlog.Err(err)) + job.Logger.Error("S3PathMigrationWorker: Failed to set success for job", mlog.Err(err)) worker.setJobError(job, err) } } func (worker *S3PathMigrationWorker) setJobError(job *model.Job, appError *model.AppError) { if err := worker.jobServer.SetJobError(job, appError); err != nil { - mlog.Error("S3PathMigrationWorker: Failed to set job error", mlog.String("worker", worker.name), mlog.String("job_id", job.Id), mlog.Err(err)) + job.Logger.Error("S3PathMigrationWorker: Failed to set job error", mlog.Err(err)) } } diff --git a/server/channels/jobs/schedulers.go b/server/channels/jobs/schedulers.go index ce718a0a612..0bfff09fa53 100644 --- a/server/channels/jobs/schedulers.go +++ b/server/channels/jobs/schedulers.go @@ -10,8 +10,15 @@ import ( "github.com/mattermost/mattermost/server/public/model" "github.com/mattermost/mattermost/server/public/shared/mlog" + "github.com/mattermost/mattermost/server/public/shared/request" ) +type Scheduler interface { + Enabled(cfg *model.Config) bool + NextScheduleTime(cfg *model.Config, now time.Time, pendingJobs bool, lastSuccessfulJob *model.Job) *time.Time + ScheduleJob(c *request.Context, cfg *model.Config, pendingJobs bool, lastSuccessfulJob *model.Job) (*model.Job, *model.AppError) +} + type Schedulers struct { stop chan bool stopped chan bool @@ -22,7 +29,7 @@ type Schedulers struct { isLeader bool running bool - schedulers map[string]model.Scheduler + schedulers map[string]Scheduler nextRunTimes map[string]*time.Time } @@ -32,7 +39,7 @@ var ( ErrSchedulersUninitialized = errors.New("job schedulers are not initialized") ) -func (schedulers *Schedulers) AddScheduler(name string, scheduler model.Scheduler) { +func (schedulers *Schedulers) AddScheduler(name string, scheduler Scheduler) { schedulers.schedulers[name] = scheduler } @@ -80,7 +87,8 @@ func (schedulers *Schedulers) Start() { if scheduler == nil || !schedulers.isLeader || !scheduler.Enabled(cfg) { continue } - if _, err := schedulers.scheduleJob(cfg, name, scheduler); err != nil { + c := request.EmptyContext(schedulers.jobs.Logger()) + if _, err := schedulers.scheduleJob(c, cfg, name, scheduler); err != nil { mlog.Error("Failed to schedule job", mlog.String("scheduler", name), mlog.Err(err)) continue } @@ -147,7 +155,7 @@ func (schedulers *Schedulers) setNextRunTime(cfg *model.Config, name string, now mlog.Debug("Next run time for scheduler", mlog.String("scheduler_name", name), mlog.String("next_runtime", fmt.Sprintf("%v", schedulers.nextRunTimes[name]))) } -func (schedulers *Schedulers) scheduleJob(cfg *model.Config, name string, scheduler model.Scheduler) (*model.Job, *model.AppError) { +func (schedulers *Schedulers) scheduleJob(c *request.Context, cfg *model.Config, name string, scheduler Scheduler) (*model.Job, *model.AppError) { pendingJobs, err := schedulers.jobs.CheckForPendingJobsByType(name) if err != nil { return nil, err @@ -158,7 +166,7 @@ func (schedulers *Schedulers) scheduleJob(cfg *model.Config, name string, schedu return nil, err } - return scheduler.ScheduleJob(cfg, pendingJobs, lastSuccessfulJob) + return scheduler.ScheduleJob(c, cfg, pendingJobs, lastSuccessfulJob) } func (schedulers *Schedulers) handleConfigChange(_, newConfig *model.Config) { diff --git a/server/channels/jobs/schedulers_test.go b/server/channels/jobs/schedulers_test.go index 09356829374..b813b0af6d0 100644 --- a/server/channels/jobs/schedulers_test.go +++ b/server/channels/jobs/schedulers_test.go @@ -12,6 +12,7 @@ import ( "github.com/mattermost/mattermost/server/public/model" "github.com/mattermost/mattermost/server/public/plugin/plugintest/mock" + "github.com/mattermost/mattermost/server/public/shared/request" "github.com/mattermost/mattermost/server/v8/channels/store/storetest" "github.com/mattermost/mattermost/server/v8/channels/utils/testutils" ) @@ -29,7 +30,7 @@ func (scheduler *MockScheduler) NextScheduleTime(cfg *model.Config, now time.Tim return &nextTime } -func (scheduler *MockScheduler) ScheduleJob(cfg *model.Config, pendingJobs bool, lastSuccessfulJob *model.Job) (*model.Job, *model.AppError) { +func (scheduler *MockScheduler) ScheduleJob(c *request.Context, cfg *model.Config, pendingJobs bool, lastSuccessfulJob *model.Job) (*model.Job, *model.AppError) { return nil, nil } diff --git a/server/channels/jobs/server.go b/server/channels/jobs/server.go index 1f80cc5038c..e87763319d4 100644 --- a/server/channels/jobs/server.go +++ b/server/channels/jobs/server.go @@ -8,6 +8,7 @@ import ( "time" "github.com/mattermost/mattermost/server/public/model" + "github.com/mattermost/mattermost/server/public/shared/mlog" "github.com/mattermost/mattermost/server/v8/channels/store" "github.com/mattermost/mattermost/server/v8/einterfaces" "github.com/mattermost/mattermost/server/v8/platform/services/configservice" @@ -17,6 +18,7 @@ type JobServer struct { ConfigService configservice.ConfigService Store store.Store metrics einterfaces.MetricsInterface + logger mlog.LoggerIFace // mut is used to protect the following fields from concurrent access. mut sync.Mutex @@ -24,11 +26,12 @@ type JobServer struct { schedulers *Schedulers } -func NewJobServer(configService configservice.ConfigService, store store.Store, metrics einterfaces.MetricsInterface) *JobServer { +func NewJobServer(configService configservice.ConfigService, store store.Store, metrics einterfaces.MetricsInterface, logger mlog.LoggerIFace) *JobServer { srv := &JobServer{ ConfigService: configService, Store: store, metrics: metrics, + logger: logger, } srv.initWorkers() srv.initSchedulers() @@ -47,7 +50,7 @@ func (srv *JobServer) initSchedulers() { clusterLeaderChanged: make(chan bool, 1), jobs: srv, isLeader: true, - schedulers: make(map[string]model.Scheduler), + schedulers: make(map[string]Scheduler), nextRunTimes: make(map[string]*time.Time), } @@ -58,7 +61,11 @@ func (srv *JobServer) Config() *model.Config { return srv.ConfigService.Config() } -func (srv *JobServer) RegisterJobType(name string, worker model.Worker, scheduler model.Scheduler) { +func (srv *JobServer) Logger() mlog.LoggerIFace { + return srv.logger +} + +func (srv *JobServer) RegisterJobType(name string, worker model.Worker, scheduler Scheduler) { srv.mut.Lock() defer srv.mut.Unlock() if worker != nil { diff --git a/server/channels/product/api.go b/server/channels/product/api.go index de886aae841..60f24927eb6 100644 --- a/server/channels/product/api.go +++ b/server/channels/product/api.go @@ -10,7 +10,7 @@ import ( "github.com/mattermost/mattermost/server/public/model" "github.com/mattermost/mattermost/server/public/shared/mlog" - "github.com/mattermost/mattermost/server/v8/channels/app/request" + "github.com/mattermost/mattermost/server/public/shared/request" "github.com/mattermost/mattermost/server/v8/platform/shared/filestore" ) diff --git a/server/channels/store/layer_generators/opentracing_layer.go.tmpl b/server/channels/store/layer_generators/opentracing_layer.go.tmpl index 02d7408df9f..f540421a87a 100644 --- a/server/channels/store/layer_generators/opentracing_layer.go.tmpl +++ b/server/channels/store/layer_generators/opentracing_layer.go.tmpl @@ -10,6 +10,7 @@ import ( "context" "github.com/mattermost/mattermost/server/public/model" + "github.com/mattermost/mattermost/server/public/shared/request" "github.com/mattermost/mattermost/server/v8/platform/services/tracing" "github.com/mattermost/mattermost/server/v8/channels/store" "github.com/opentracing/opentracing-go/ext" diff --git a/server/channels/store/layer_generators/retry_layer.go.tmpl b/server/channels/store/layer_generators/retry_layer.go.tmpl index dd8fdac7b24..bb6d69e4d82 100644 --- a/server/channels/store/layer_generators/retry_layer.go.tmpl +++ b/server/channels/store/layer_generators/retry_layer.go.tmpl @@ -10,11 +10,13 @@ import ( "context" timepkg "time" - "github.com/lib/pq" - "github.com/mattermost/mattermost/server/public/model" - "github.com/mattermost/mattermost/server/v8/channels/store" - "github.com/pkg/errors" "github.com/go-sql-driver/mysql" + "github.com/lib/pq" + "github.com/pkg/errors" + + "github.com/mattermost/mattermost/server/public/model" + "github.com/mattermost/mattermost/server/public/shared/request" + "github.com/mattermost/mattermost/server/v8/channels/store" ) const mySQLDeadlockCode = uint16(1213) diff --git a/server/channels/store/layer_generators/timer_layer.go.tmpl b/server/channels/store/layer_generators/timer_layer.go.tmpl index daef4c61da5..f51c50e0931 100644 --- a/server/channels/store/layer_generators/timer_layer.go.tmpl +++ b/server/channels/store/layer_generators/timer_layer.go.tmpl @@ -10,9 +10,10 @@ import ( "context" "time" - "github.com/mattermost/mattermost/server/v8/einterfaces" "github.com/mattermost/mattermost/server/public/model" + "github.com/mattermost/mattermost/server/public/shared/request" "github.com/mattermost/mattermost/server/v8/channels/store" + "github.com/mattermost/mattermost/server/v8/einterfaces" ) type {{.Name}} struct { diff --git a/server/channels/store/localcachelayer/channel_layer_test.go b/server/channels/store/localcachelayer/channel_layer_test.go index 76aefe18790..181aa790322 100644 --- a/server/channels/store/localcachelayer/channel_layer_test.go +++ b/server/channels/store/localcachelayer/channel_layer_test.go @@ -22,7 +22,7 @@ func TestChannelStoreChannelMemberCountsCache(t *testing.T) { countResult := int64(10) t.Run("first call not cached, second cached and returning same data", func(t *testing.T) { - mockStore := getMockStore() + mockStore := getMockStore(t) mockCacheProvider := getMockCacheProvider() cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider) require.NoError(t, err) @@ -38,7 +38,7 @@ func TestChannelStoreChannelMemberCountsCache(t *testing.T) { }) t.Run("first call not cached, second force not cached", func(t *testing.T) { - mockStore := getMockStore() + mockStore := getMockStore(t) mockCacheProvider := getMockCacheProvider() cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider) require.NoError(t, err) @@ -50,7 +50,7 @@ func TestChannelStoreChannelMemberCountsCache(t *testing.T) { }) t.Run("first call force not cached, second not cached, third cached", func(t *testing.T) { - mockStore := getMockStore() + mockStore := getMockStore(t) mockCacheProvider := getMockCacheProvider() cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider) require.NoError(t, err) @@ -64,7 +64,7 @@ func TestChannelStoreChannelMemberCountsCache(t *testing.T) { }) t.Run("first call with GetMemberCountFromCache not cached, second cached and returning same data", func(t *testing.T) { - mockStore := getMockStore() + mockStore := getMockStore(t) mockCacheProvider := getMockCacheProvider() cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider) require.NoError(t, err) @@ -78,7 +78,7 @@ func TestChannelStoreChannelMemberCountsCache(t *testing.T) { }) t.Run("first call not cached, clear cache, second call not cached", func(t *testing.T) { - mockStore := getMockStore() + mockStore := getMockStore(t) mockCacheProvider := getMockCacheProvider() cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider) require.NoError(t, err) @@ -91,7 +91,7 @@ func TestChannelStoreChannelMemberCountsCache(t *testing.T) { }) t.Run("first call not cached, invalidate cache, second call not cached", func(t *testing.T) { - mockStore := getMockStore() + mockStore := getMockStore(t) mockCacheProvider := getMockCacheProvider() cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider) require.NoError(t, err) @@ -111,7 +111,7 @@ func TestChannelStoreChannelsMemberCountCache(t *testing.T) { } t.Run("first call not cached, second cached and returning same data", func(t *testing.T) { - mockStore := getMockStore() + mockStore := getMockStore(t) mockCacheProvider := getMockCacheProvider() cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider) require.NoError(t, err) @@ -127,7 +127,7 @@ func TestChannelStoreChannelsMemberCountCache(t *testing.T) { }) t.Run("first call not cached, invalidate cache, second call not cached", func(t *testing.T) { - mockStore := getMockStore() + mockStore := getMockStore(t) mockCacheProvider := getMockCacheProvider() cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider) require.NoError(t, err) @@ -145,7 +145,7 @@ func TestChannelStoreChannelPinnedPostsCountsCache(t *testing.T) { countResult := int64(10) t.Run("first call not cached, second cached and returning same data", func(t *testing.T) { - mockStore := getMockStore() + mockStore := getMockStore(t) mockCacheProvider := getMockCacheProvider() cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider) require.NoError(t, err) @@ -161,7 +161,7 @@ func TestChannelStoreChannelPinnedPostsCountsCache(t *testing.T) { }) t.Run("first call not cached, second force not cached", func(t *testing.T) { - mockStore := getMockStore() + mockStore := getMockStore(t) mockCacheProvider := getMockCacheProvider() cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider) require.NoError(t, err) @@ -173,7 +173,7 @@ func TestChannelStoreChannelPinnedPostsCountsCache(t *testing.T) { }) t.Run("first call force not cached, second not cached, third cached", func(t *testing.T) { - mockStore := getMockStore() + mockStore := getMockStore(t) mockCacheProvider := getMockCacheProvider() cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider) require.NoError(t, err) @@ -187,7 +187,7 @@ func TestChannelStoreChannelPinnedPostsCountsCache(t *testing.T) { }) t.Run("first call not cached, clear cache, second call not cached", func(t *testing.T) { - mockStore := getMockStore() + mockStore := getMockStore(t) mockCacheProvider := getMockCacheProvider() cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider) require.NoError(t, err) @@ -200,7 +200,7 @@ func TestChannelStoreChannelPinnedPostsCountsCache(t *testing.T) { }) t.Run("first call not cached, invalidate cache, second call not cached", func(t *testing.T) { - mockStore := getMockStore() + mockStore := getMockStore(t) mockCacheProvider := getMockCacheProvider() cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider) require.NoError(t, err) @@ -217,7 +217,7 @@ func TestChannelStoreGuestCountCache(t *testing.T) { countResult := int64(12) t.Run("first call not cached, second cached and returning same data", func(t *testing.T) { - mockStore := getMockStore() + mockStore := getMockStore(t) mockCacheProvider := getMockCacheProvider() cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider) require.NoError(t, err) @@ -233,7 +233,7 @@ func TestChannelStoreGuestCountCache(t *testing.T) { }) t.Run("first call not cached, second force not cached", func(t *testing.T) { - mockStore := getMockStore() + mockStore := getMockStore(t) mockCacheProvider := getMockCacheProvider() cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider) require.NoError(t, err) @@ -245,7 +245,7 @@ func TestChannelStoreGuestCountCache(t *testing.T) { }) t.Run("first call force not cached, second not cached, third cached", func(t *testing.T) { - mockStore := getMockStore() + mockStore := getMockStore(t) mockCacheProvider := getMockCacheProvider() cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider) require.NoError(t, err) @@ -259,7 +259,7 @@ func TestChannelStoreGuestCountCache(t *testing.T) { }) t.Run("first call not cached, clear cache, second call not cached", func(t *testing.T) { - mockStore := getMockStore() + mockStore := getMockStore(t) mockCacheProvider := getMockCacheProvider() cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider) require.NoError(t, err) @@ -272,7 +272,7 @@ func TestChannelStoreGuestCountCache(t *testing.T) { }) t.Run("first call not cached, invalidate cache, second call not cached", func(t *testing.T) { - mockStore := getMockStore() + mockStore := getMockStore(t) mockCacheProvider := getMockCacheProvider() cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider) require.NoError(t, err) @@ -289,7 +289,7 @@ func TestChannelStoreChannel(t *testing.T) { channelId := "channel1" fakeChannel := model.Channel{Id: channelId} t.Run("first call by id not cached, second cached and returning same data", func(t *testing.T) { - mockStore := getMockStore() + mockStore := getMockStore(t) mockCacheProvider := getMockCacheProvider() cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider) require.NoError(t, err) @@ -305,7 +305,7 @@ func TestChannelStoreChannel(t *testing.T) { }) t.Run("first call not cached, second force no cached", func(t *testing.T) { - mockStore := getMockStore() + mockStore := getMockStore(t) mockCacheProvider := getMockCacheProvider() cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider) require.NoError(t, err) @@ -317,7 +317,7 @@ func TestChannelStoreChannel(t *testing.T) { }) t.Run("first call force no cached, second not cached, third cached", func(t *testing.T) { - mockStore := getMockStore() + mockStore := getMockStore(t) mockCacheProvider := getMockCacheProvider() cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider) require.NoError(t, err) @@ -330,7 +330,7 @@ func TestChannelStoreChannel(t *testing.T) { }) t.Run("first call not cached, clear cache, second call not cached", func(t *testing.T) { - mockStore := getMockStore() + mockStore := getMockStore(t) mockCacheProvider := getMockCacheProvider() cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider) require.NoError(t, err) @@ -343,7 +343,7 @@ func TestChannelStoreChannel(t *testing.T) { }) t.Run("first call not cached, invalidate cache, second call not cached", func(t *testing.T) { - mockStore := getMockStore() + mockStore := getMockStore(t) mockCacheProvider := getMockCacheProvider() cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider) require.NoError(t, err) diff --git a/server/channels/store/localcachelayer/emoji_layer.go b/server/channels/store/localcachelayer/emoji_layer.go index cb5b4659c60..5c70c0f8045 100644 --- a/server/channels/store/localcachelayer/emoji_layer.go +++ b/server/channels/store/localcachelayer/emoji_layer.go @@ -5,10 +5,10 @@ package localcachelayer import ( "bytes" - "context" "sync" "github.com/mattermost/mattermost/server/public/model" + "github.com/mattermost/mattermost/server/public/shared/request" "github.com/mattermost/mattermost/server/v8/channels/store" "github.com/mattermost/mattermost/server/v8/channels/store/sqlstore" ) @@ -44,7 +44,7 @@ func (es *LocalCacheEmojiStore) handleClusterInvalidateEmojiIdByName(msg *model. } } -func (es *LocalCacheEmojiStore) Get(ctx context.Context, id string, allowFromCache bool) (*model.Emoji, error) { +func (es *LocalCacheEmojiStore) Get(c request.CTX, id string, allowFromCache bool) (*model.Emoji, error) { if allowFromCache { if emoji, ok := es.getFromCacheById(id); ok { return emoji, nil @@ -55,12 +55,12 @@ func (es *LocalCacheEmojiStore) Get(ctx context.Context, id string, allowFromCac es.emojiByIdMut.Lock() if es.emojiByIdInvalidations[id] { // And then remove the key from the map. - ctx = sqlstore.WithMaster(ctx) + c = sqlstore.RequestContextWithMaster(c) delete(es.emojiByIdInvalidations, id) } es.emojiByIdMut.Unlock() - emoji, err := es.EmojiStore.Get(ctx, id, allowFromCache) + emoji, err := es.EmojiStore.Get(c, id, allowFromCache) if allowFromCache && err == nil { es.addToCache(emoji) @@ -69,9 +69,9 @@ func (es *LocalCacheEmojiStore) Get(ctx context.Context, id string, allowFromCac return emoji, err } -func (es *LocalCacheEmojiStore) GetByName(ctx context.Context, name string, allowFromCache bool) (*model.Emoji, error) { +func (es *LocalCacheEmojiStore) GetByName(c request.CTX, name string, allowFromCache bool) (*model.Emoji, error) { if id, ok := model.GetSystemEmojiId(name); ok { - return es.Get(ctx, id, allowFromCache) + return es.Get(c, id, allowFromCache) } if allowFromCache { @@ -83,13 +83,13 @@ func (es *LocalCacheEmojiStore) GetByName(ctx context.Context, name string, allo // If it was invalidated, then we need to query master. es.emojiByNameMut.Lock() if es.emojiByNameInvalidations[name] { - ctx = sqlstore.WithMaster(ctx) + c = sqlstore.RequestContextWithMaster(c) // And then remove the key from the map. delete(es.emojiByNameInvalidations, name) } es.emojiByNameMut.Unlock() - emoji, err := es.EmojiStore.GetByName(ctx, name, allowFromCache) + emoji, err := es.EmojiStore.GetByName(c, name, allowFromCache) if err != nil { return nil, err } @@ -101,7 +101,7 @@ func (es *LocalCacheEmojiStore) GetByName(ctx context.Context, name string, allo return emoji, nil } -func (es *LocalCacheEmojiStore) GetMultipleByName(ctx context.Context, names []string) ([]*model.Emoji, error) { +func (es *LocalCacheEmojiStore) GetMultipleByName(c request.CTX, names []string) ([]*model.Emoji, error) { emojis := []*model.Emoji{} remainingEmojiNames := make([]string, 0) @@ -112,7 +112,7 @@ func (es *LocalCacheEmojiStore) GetMultipleByName(ctx context.Context, names []s // If it was invalidated, then we need to query master. es.emojiByNameMut.Lock() if es.emojiByNameInvalidations[name] { - ctx = sqlstore.WithMaster(ctx) + c = sqlstore.RequestContextWithMaster(c) // And then remove the key from the map. delete(es.emojiByNameInvalidations, name) } @@ -123,7 +123,7 @@ func (es *LocalCacheEmojiStore) GetMultipleByName(ctx context.Context, names []s } if len(remainingEmojiNames) > 0 { - remainingEmojis, err := es.EmojiStore.GetMultipleByName(ctx, remainingEmojiNames) + remainingEmojis, err := es.EmojiStore.GetMultipleByName(c, remainingEmojiNames) if err != nil { return nil, err } diff --git a/server/channels/store/localcachelayer/emoji_layer_test.go b/server/channels/store/localcachelayer/emoji_layer_test.go index f3b643ff7b1..1258982e1d3 100644 --- a/server/channels/store/localcachelayer/emoji_layer_test.go +++ b/server/channels/store/localcachelayer/emoji_layer_test.go @@ -4,13 +4,13 @@ package localcachelayer import ( - "context" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/mattermost/mattermost/server/public/model" + "github.com/mattermost/mattermost/server/public/shared/request" "github.com/mattermost/mattermost/server/v8/channels/store/storetest" "github.com/mattermost/mattermost/server/v8/channels/store/storetest/mocks" ) @@ -25,49 +25,52 @@ func TestEmojiStoreCache(t *testing.T) { ctxEmoji := model.Emoji{Id: "master", Name: "name123"} t.Run("first call by id not cached, second cached and returning same data", func(t *testing.T) { - mockStore := getMockStore() + c := request.TestContext(t) + mockStore := getMockStore(t) mockCacheProvider := getMockCacheProvider() cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider) require.NoError(t, err) - emoji, err := cachedStore.Emoji().Get(context.Background(), "123", true) + emoji, err := cachedStore.Emoji().Get(c, "123", true) require.NoError(t, err) assert.Equal(t, emoji, &fakeEmoji) mockStore.Emoji().(*mocks.EmojiStore).AssertNumberOfCalls(t, "Get", 1) - emoji, err = cachedStore.Emoji().Get(context.Background(), "123", true) + emoji, err = cachedStore.Emoji().Get(c, "123", true) require.NoError(t, err) assert.Equal(t, emoji, &fakeEmoji) mockStore.Emoji().(*mocks.EmojiStore).AssertNumberOfCalls(t, "Get", 1) }) t.Run("GetByName: first call by name not cached, second cached and returning same data", func(t *testing.T) { - mockStore := getMockStore() + c := request.TestContext(t) + mockStore := getMockStore(t) mockCacheProvider := getMockCacheProvider() cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider) require.NoError(t, err) - emoji, err := cachedStore.Emoji().GetByName(context.Background(), "name123", true) + emoji, err := cachedStore.Emoji().GetByName(c, "name123", true) require.NoError(t, err) assert.Equal(t, emoji, &fakeEmoji) mockStore.Emoji().(*mocks.EmojiStore).AssertNumberOfCalls(t, "GetByName", 1) - emoji, err = cachedStore.Emoji().GetByName(context.Background(), "name123", true) + emoji, err = cachedStore.Emoji().GetByName(c, "name123", true) require.NoError(t, err) assert.Equal(t, emoji, &fakeEmoji) mockStore.Emoji().(*mocks.EmojiStore).AssertNumberOfCalls(t, "GetByName", 1) }) t.Run("GetMultipleByName: first call by name not cached, second cached and returning same data", func(t *testing.T) { - mockStore := getMockStore() + c := request.TestContext(t) + mockStore := getMockStore(t) mockCacheProvider := getMockCacheProvider() cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider) require.NoError(t, err) - emojis, err := cachedStore.Emoji().GetMultipleByName(context.Background(), []string{"name123"}) + emojis, err := cachedStore.Emoji().GetMultipleByName(c, []string{"name123"}) require.NoError(t, err) require.Len(t, emojis, 1) assert.Equal(t, emojis[0], &fakeEmoji) mockStore.Emoji().(*mocks.EmojiStore).AssertNumberOfCalls(t, "GetMultipleByName", 1) - emojis, err = cachedStore.Emoji().GetMultipleByName(context.Background(), []string{"name123"}) + emojis, err = cachedStore.Emoji().GetMultipleByName(c, []string{"name123"}) require.NoError(t, err) require.Len(t, emojis, 1) assert.Equal(t, emojis[0], &fakeEmoji) @@ -75,18 +78,19 @@ func TestEmojiStoreCache(t *testing.T) { }) t.Run("GetMultipleByName: multiple elements", func(t *testing.T) { - mockStore := getMockStore() + c := request.TestContext(t) + mockStore := getMockStore(t) mockCacheProvider := getMockCacheProvider() cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider) require.NoError(t, err) - emojis, err := cachedStore.Emoji().GetMultipleByName(context.Background(), []string{"name123", "name321"}) + emojis, err := cachedStore.Emoji().GetMultipleByName(c, []string{"name123", "name321"}) require.NoError(t, err) require.Len(t, emojis, 2) assert.Equal(t, emojis[0], &fakeEmoji) assert.Equal(t, emojis[1], &fakeEmoji2) mockStore.Emoji().(*mocks.EmojiStore).AssertNumberOfCalls(t, "GetMultipleByName", 1) - emojis, err = cachedStore.Emoji().GetMultipleByName(context.Background(), []string{"name123"}) + emojis, err = cachedStore.Emoji().GetMultipleByName(c, []string{"name123"}) require.NoError(t, err) require.Len(t, emojis, 1) assert.Equal(t, emojis[0], &fakeEmoji) @@ -94,132 +98,142 @@ func TestEmojiStoreCache(t *testing.T) { }) t.Run("first call by id not cached, second force not cached", func(t *testing.T) { - mockStore := getMockStore() + c := request.TestContext(t) + mockStore := getMockStore(t) mockCacheProvider := getMockCacheProvider() cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider) require.NoError(t, err) - cachedStore.Emoji().Get(context.Background(), "123", true) + cachedStore.Emoji().Get(c, "123", true) mockStore.Emoji().(*mocks.EmojiStore).AssertNumberOfCalls(t, "Get", 1) - cachedStore.Emoji().Get(context.Background(), "123", false) + cachedStore.Emoji().Get(c, "123", false) mockStore.Emoji().(*mocks.EmojiStore).AssertNumberOfCalls(t, "Get", 2) }) t.Run("first call by name not cached, second force not cached", func(t *testing.T) { - mockStore := getMockStore() + c := request.TestContext(t) + mockStore := getMockStore(t) mockCacheProvider := getMockCacheProvider() cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider) require.NoError(t, err) - cachedStore.Emoji().GetByName(context.Background(), "name123", true) + cachedStore.Emoji().GetByName(c, "name123", true) mockStore.Emoji().(*mocks.EmojiStore).AssertNumberOfCalls(t, "GetByName", 1) - cachedStore.Emoji().GetByName(context.Background(), "name123", false) + cachedStore.Emoji().GetByName(c, "name123", false) mockStore.Emoji().(*mocks.EmojiStore).AssertNumberOfCalls(t, "GetByName", 2) }) t.Run("first call by id force not cached, second not cached, third cached", func(t *testing.T) { - mockStore := getMockStore() + c := request.TestContext(t) + mockStore := getMockStore(t) mockCacheProvider := getMockCacheProvider() cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider) require.NoError(t, err) - cachedStore.Emoji().Get(context.Background(), "123", false) + cachedStore.Emoji().Get(c, "123", false) mockStore.Emoji().(*mocks.EmojiStore).AssertNumberOfCalls(t, "Get", 1) - cachedStore.Emoji().Get(context.Background(), "123", true) + cachedStore.Emoji().Get(c, "123", true) mockStore.Emoji().(*mocks.EmojiStore).AssertNumberOfCalls(t, "Get", 2) - cachedStore.Emoji().Get(context.Background(), "123", true) + cachedStore.Emoji().Get(c, "123", true) mockStore.Emoji().(*mocks.EmojiStore).AssertNumberOfCalls(t, "Get", 2) }) t.Run("first call by name force not cached, second not cached, third cached", func(t *testing.T) { - mockStore := getMockStore() + c := request.TestContext(t) + mockStore := getMockStore(t) mockCacheProvider := getMockCacheProvider() cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider) require.NoError(t, err) - cachedStore.Emoji().GetByName(context.Background(), "name123", false) + cachedStore.Emoji().GetByName(c, "name123", false) mockStore.Emoji().(*mocks.EmojiStore).AssertNumberOfCalls(t, "GetByName", 1) - cachedStore.Emoji().GetByName(context.Background(), "name123", true) + cachedStore.Emoji().GetByName(c, "name123", true) mockStore.Emoji().(*mocks.EmojiStore).AssertNumberOfCalls(t, "GetByName", 2) - cachedStore.Emoji().GetByName(context.Background(), "name123", true) + cachedStore.Emoji().GetByName(c, "name123", true) mockStore.Emoji().(*mocks.EmojiStore).AssertNumberOfCalls(t, "GetByName", 2) }) t.Run("first call by id, second call by name and GetMultipleByName cached", func(t *testing.T) { - mockStore := getMockStore() + c := request.TestContext(t) + mockStore := getMockStore(t) mockCacheProvider := getMockCacheProvider() cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider) require.NoError(t, err) - cachedStore.Emoji().Get(context.Background(), "123", true) + cachedStore.Emoji().Get(c, "123", true) mockStore.Emoji().(*mocks.EmojiStore).AssertNumberOfCalls(t, "Get", 1) - cachedStore.Emoji().GetByName(context.Background(), "name123", true) + cachedStore.Emoji().GetByName(c, "name123", true) mockStore.Emoji().(*mocks.EmojiStore).AssertNumberOfCalls(t, "GetByName", 0) - cachedStore.Emoji().GetMultipleByName(context.Background(), []string{"name123"}) + cachedStore.Emoji().GetMultipleByName(c, []string{"name123"}) mockStore.Emoji().(*mocks.EmojiStore).AssertNumberOfCalls(t, "GetMultipleByName", 0) }) t.Run("first call by name, second call by id cached", func(t *testing.T) { - mockStore := getMockStore() + c := request.TestContext(t) + mockStore := getMockStore(t) mockCacheProvider := getMockCacheProvider() cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider) require.NoError(t, err) - cachedStore.Emoji().GetByName(context.Background(), "name123", true) + cachedStore.Emoji().GetByName(c, "name123", true) mockStore.Emoji().(*mocks.EmojiStore).AssertNumberOfCalls(t, "GetByName", 1) - cachedStore.Emoji().Get(context.Background(), "123", true) + cachedStore.Emoji().Get(c, "123", true) mockStore.Emoji().(*mocks.EmojiStore).AssertNumberOfCalls(t, "Get", 0) }) t.Run("first call by id not cached, invalidate, and then not cached again", func(t *testing.T) { - mockStore := getMockStore() + c := request.TestContext(t) + mockStore := getMockStore(t) mockCacheProvider := getMockCacheProvider() cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider) require.NoError(t, err) - cachedStore.Emoji().Get(context.Background(), "123", true) + cachedStore.Emoji().Get(c, "123", true) mockStore.Emoji().(*mocks.EmojiStore).AssertNumberOfCalls(t, "Get", 1) cachedStore.Emoji().Delete(&fakeEmoji, 0) - cachedStore.Emoji().Get(context.Background(), "123", true) + cachedStore.Emoji().Get(c, "123", true) mockStore.Emoji().(*mocks.EmojiStore).AssertNumberOfCalls(t, "Get", 2) }) t.Run("call by id, use master", func(t *testing.T) { - mockStore := getMockStore() + c := request.TestContext(t) + mockStore := getMockStore(t) mockCacheProvider := getMockCacheProvider() cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider) require.NoError(t, err) - cachedStore.Emoji().Get(context.Background(), "master", true) + cachedStore.Emoji().Get(c, "master", true) mockStore.Emoji().(*mocks.EmojiStore).AssertNumberOfCalls(t, "Get", 1) cachedStore.Emoji().Delete(&ctxEmoji, 0) - cachedStore.Emoji().Get(context.Background(), "master", true) + cachedStore.Emoji().Get(c, "master", true) mockStore.Emoji().(*mocks.EmojiStore).AssertNumberOfCalls(t, "Get", 2) }) t.Run("first call by name not cached, invalidate, and then not cached again", func(t *testing.T) { - mockStore := getMockStore() + c := request.TestContext(t) + mockStore := getMockStore(t) mockCacheProvider := getMockCacheProvider() cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider) require.NoError(t, err) - cachedStore.Emoji().GetByName(context.Background(), "name123", true) + cachedStore.Emoji().GetByName(c, "name123", true) mockStore.Emoji().(*mocks.EmojiStore).AssertNumberOfCalls(t, "GetByName", 1) cachedStore.Emoji().Delete(&fakeEmoji, 0) - cachedStore.Emoji().GetByName(context.Background(), "name123", true) + cachedStore.Emoji().GetByName(c, "name123", true) mockStore.Emoji().(*mocks.EmojiStore).AssertNumberOfCalls(t, "GetByName", 2) }) t.Run("call by name, use master", func(t *testing.T) { - mockStore := getMockStore() + c := request.TestContext(t) + mockStore := getMockStore(t) mockCacheProvider := getMockCacheProvider() cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider) require.NoError(t, err) - cachedStore.Emoji().GetByName(context.Background(), "master", true) + cachedStore.Emoji().GetByName(c, "master", true) mockStore.Emoji().(*mocks.EmojiStore).AssertNumberOfCalls(t, "GetByName", 1) cachedStore.Emoji().Delete(&ctxEmoji, 0) - cachedStore.Emoji().GetByName(context.Background(), "master", true) + cachedStore.Emoji().GetByName(c, "master", true) mockStore.Emoji().(*mocks.EmojiStore).AssertNumberOfCalls(t, "GetByName", 2) }) } diff --git a/server/channels/store/localcachelayer/file_info_layer_test.go b/server/channels/store/localcachelayer/file_info_layer_test.go index a298d0b0a30..0a589eb0385 100644 --- a/server/channels/store/localcachelayer/file_info_layer_test.go +++ b/server/channels/store/localcachelayer/file_info_layer_test.go @@ -22,7 +22,7 @@ func TestFileInfoStoreCache(t *testing.T) { fakeFileInfo := model.FileInfo{PostId: "123"} t.Run("first call not cached, second cached and returning same data", func(t *testing.T) { - mockStore := getMockStore() + mockStore := getMockStore(t) mockCacheProvider := getMockCacheProvider() cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider) require.NoError(t, err) @@ -37,7 +37,7 @@ func TestFileInfoStoreCache(t *testing.T) { }) t.Run("first call not cached, second force no cached", func(t *testing.T) { - mockStore := getMockStore() + mockStore := getMockStore(t) mockCacheProvider := getMockCacheProvider() cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider) require.NoError(t, err) @@ -49,7 +49,7 @@ func TestFileInfoStoreCache(t *testing.T) { }) t.Run("first call not cached, invalidate, and then not cached again", func(t *testing.T) { - mockStore := getMockStore() + mockStore := getMockStore(t) mockCacheProvider := getMockCacheProvider() cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider) require.NoError(t, err) diff --git a/server/channels/store/localcachelayer/main_test.go b/server/channels/store/localcachelayer/main_test.go index 9fc4fb33852..e82ae0a7806 100644 --- a/server/channels/store/localcachelayer/main_test.go +++ b/server/channels/store/localcachelayer/main_test.go @@ -11,6 +11,7 @@ import ( "github.com/stretchr/testify/mock" "github.com/mattermost/mattermost/server/public/model" + "github.com/mattermost/mattermost/server/public/shared/request" "github.com/mattermost/mattermost/server/v8/channels/store" "github.com/mattermost/mattermost/server/v8/channels/store/sqlstore" "github.com/mattermost/mattermost/server/v8/channels/store/storetest/mocks" @@ -28,7 +29,7 @@ func getMockCacheProvider() cache.Provider { return &mockCacheProvider } -func getMockStore() *mocks.Store { +func getMockStore(t *testing.T) *mocks.Store { mockStore := mocks.Store{} fakeReaction := model.Reaction{PostId: "123"} @@ -74,14 +75,14 @@ func getMockStore() *mocks.Store { mockEmojiStore := mocks.EmojiStore{} mockEmojiStore.On("Get", mock.Anything, "123", true).Return(&fakeEmoji, nil) mockEmojiStore.On("Get", mock.Anything, "123", false).Return(&fakeEmoji, nil) - mockEmojiStore.On("Get", context.Background(), "master", true).Return(&ctxEmoji, nil) - mockEmojiStore.On("Get", sqlstore.WithMaster(context.Background()), "master", true).Return(&ctxEmoji, nil) + mockEmojiStore.On("Get", mock.IsType(&request.Context{}), "master", true).Return(&ctxEmoji, nil) + mockEmojiStore.On("Get", sqlstore.RequestContextWithMaster(request.TestContext(t)), "master", true).Return(&ctxEmoji, nil) mockEmojiStore.On("GetByName", mock.Anything, "name123", true).Return(&fakeEmoji, nil) mockEmojiStore.On("GetByName", mock.Anything, "name123", false).Return(&fakeEmoji, nil) - mockEmojiStore.On("GetMultipleByName", context.Background(), []string{"name123"}).Return([]*model.Emoji{&fakeEmoji}, nil) - mockEmojiStore.On("GetMultipleByName", context.Background(), []string{"name123", "name321"}).Return([]*model.Emoji{&fakeEmoji, &fakeEmoji2}, nil) - mockEmojiStore.On("GetByName", context.Background(), "master", true).Return(&ctxEmoji, nil) - mockEmojiStore.On("GetByName", sqlstore.WithMaster(context.Background()), "master", false).Return(&ctxEmoji, nil) + mockEmojiStore.On("GetMultipleByName", mock.IsType(&request.Context{}), []string{"name123"}).Return([]*model.Emoji{&fakeEmoji}, nil) + mockEmojiStore.On("GetMultipleByName", mock.IsType(&request.Context{}), []string{"name123", "name321"}).Return([]*model.Emoji{&fakeEmoji, &fakeEmoji2}, nil) + mockEmojiStore.On("GetByName", mock.IsType(&request.Context{}), "master", true).Return(&ctxEmoji, nil) + mockEmojiStore.On("GetByName", sqlstore.RequestContextWithMaster(request.TestContext(t)), "master", false).Return(&ctxEmoji, nil) mockEmojiStore.On("Delete", &fakeEmoji, int64(0)).Return(nil) mockEmojiStore.On("Delete", &ctxEmoji, int64(0)).Return(nil) mockStore.On("Emoji").Return(&mockEmojiStore) diff --git a/server/channels/store/localcachelayer/post_layer_test.go b/server/channels/store/localcachelayer/post_layer_test.go index 213b4c8541f..19e9caa8e70 100644 --- a/server/channels/store/localcachelayer/post_layer_test.go +++ b/server/channels/store/localcachelayer/post_layer_test.go @@ -29,7 +29,7 @@ func TestPostStoreLastPostTimeCache(t *testing.T) { } t.Run("GetEtag: first call not cached, second cached and returning same data", func(t *testing.T) { - mockStore := getMockStore() + mockStore := getMockStore(t) mockCacheProvider := getMockCacheProvider() cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider) require.NoError(t, err) @@ -46,7 +46,7 @@ func TestPostStoreLastPostTimeCache(t *testing.T) { }) t.Run("GetEtag: first call not cached, second force no cached", func(t *testing.T) { - mockStore := getMockStore() + mockStore := getMockStore(t) mockCacheProvider := getMockCacheProvider() cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider) require.NoError(t, err) @@ -58,7 +58,7 @@ func TestPostStoreLastPostTimeCache(t *testing.T) { }) t.Run("GetEtag: first call not cached, invalidate, and then not cached again", func(t *testing.T) { - mockStore := getMockStore() + mockStore := getMockStore(t) mockCacheProvider := getMockCacheProvider() cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider) require.NoError(t, err) @@ -71,7 +71,7 @@ func TestPostStoreLastPostTimeCache(t *testing.T) { }) t.Run("GetEtag: first call not cached, clear caches, and then not cached again", func(t *testing.T) { - mockStore := getMockStore() + mockStore := getMockStore(t) mockCacheProvider := getMockCacheProvider() cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider) require.NoError(t, err) @@ -84,7 +84,7 @@ func TestPostStoreLastPostTimeCache(t *testing.T) { }) t.Run("GetPostsSince: first call not cached, second cached and returning same data", func(t *testing.T) { - mockStore := getMockStore() + mockStore := getMockStore(t) mockCacheProvider := getMockCacheProvider() cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider) require.NoError(t, err) @@ -103,7 +103,7 @@ func TestPostStoreLastPostTimeCache(t *testing.T) { }) t.Run("GetPostsSince: first call not cached, second force no cached", func(t *testing.T) { - mockStore := getMockStore() + mockStore := getMockStore(t) mockCacheProvider := getMockCacheProvider() cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider) require.NoError(t, err) @@ -115,7 +115,7 @@ func TestPostStoreLastPostTimeCache(t *testing.T) { }) t.Run("GetPostsSince: first call not cached, invalidate, and then not cached again", func(t *testing.T) { - mockStore := getMockStore() + mockStore := getMockStore(t) mockCacheProvider := getMockCacheProvider() cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider) require.NoError(t, err) @@ -128,7 +128,7 @@ func TestPostStoreLastPostTimeCache(t *testing.T) { }) t.Run("GetPostsSince: first call not cached, clear caches, and then not cached again", func(t *testing.T) { - mockStore := getMockStore() + mockStore := getMockStore(t) mockCacheProvider := getMockCacheProvider() cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider) require.NoError(t, err) @@ -146,7 +146,7 @@ func TestPostStoreCache(t *testing.T) { fakeOptions := model.GetPostsOptions{ChannelId: "123", PerPage: 30} t.Run("first call not cached, second cached and returning same data", func(t *testing.T) { - mockStore := getMockStore() + mockStore := getMockStore(t) mockCacheProvider := getMockCacheProvider() cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider) require.NoError(t, err) @@ -161,7 +161,7 @@ func TestPostStoreCache(t *testing.T) { }) t.Run("first call not cached, second force not cached", func(t *testing.T) { - mockStore := getMockStore() + mockStore := getMockStore(t) mockCacheProvider := getMockCacheProvider() cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider) require.NoError(t, err) @@ -176,7 +176,7 @@ func TestPostStoreCache(t *testing.T) { }) t.Run("first call not cached, invalidate, and then not cached again", func(t *testing.T) { - mockStore := getMockStore() + mockStore := getMockStore(t) mockCacheProvider := getMockCacheProvider() cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider) require.NoError(t, err) diff --git a/server/channels/store/localcachelayer/reaction_layer_test.go b/server/channels/store/localcachelayer/reaction_layer_test.go index bddf67040a0..07319de3181 100644 --- a/server/channels/store/localcachelayer/reaction_layer_test.go +++ b/server/channels/store/localcachelayer/reaction_layer_test.go @@ -22,7 +22,7 @@ func TestReactionStoreCache(t *testing.T) { fakeReaction := model.Reaction{PostId: "123"} t.Run("first call not cached, second cached and returning same data", func(t *testing.T) { - mockStore := getMockStore() + mockStore := getMockStore(t) mockCacheProvider := getMockCacheProvider() cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider) require.NoError(t, err) @@ -37,7 +37,7 @@ func TestReactionStoreCache(t *testing.T) { }) t.Run("first call not cached, second force not cached", func(t *testing.T) { - mockStore := getMockStore() + mockStore := getMockStore(t) mockCacheProvider := getMockCacheProvider() cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider) require.NoError(t, err) @@ -49,7 +49,7 @@ func TestReactionStoreCache(t *testing.T) { }) t.Run("first call not cached, save, and then not cached again", func(t *testing.T) { - mockStore := getMockStore() + mockStore := getMockStore(t) mockCacheProvider := getMockCacheProvider() cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider) require.NoError(t, err) @@ -62,7 +62,7 @@ func TestReactionStoreCache(t *testing.T) { }) t.Run("first call not cached, delete, and then not cached again", func(t *testing.T) { - mockStore := getMockStore() + mockStore := getMockStore(t) mockCacheProvider := getMockCacheProvider() cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider) require.NoError(t, err) diff --git a/server/channels/store/localcachelayer/role_layer_test.go b/server/channels/store/localcachelayer/role_layer_test.go index e5053fac777..284e0c9bfa4 100644 --- a/server/channels/store/localcachelayer/role_layer_test.go +++ b/server/channels/store/localcachelayer/role_layer_test.go @@ -23,7 +23,7 @@ func TestRoleStoreCache(t *testing.T) { fakeRole := model.Role{Id: "123", Name: "role-name"} t.Run("first call not cached, second cached and returning same data", func(t *testing.T) { - mockStore := getMockStore() + mockStore := getMockStore(t) mockCacheProvider := getMockCacheProvider() cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider) require.NoError(t, err) @@ -39,7 +39,7 @@ func TestRoleStoreCache(t *testing.T) { }) t.Run("first call not cached, save, and then not cached again", func(t *testing.T) { - mockStore := getMockStore() + mockStore := getMockStore(t) mockCacheProvider := getMockCacheProvider() cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider) require.NoError(t, err) @@ -52,7 +52,7 @@ func TestRoleStoreCache(t *testing.T) { }) t.Run("first call not cached, delete, and then not cached again", func(t *testing.T) { - mockStore := getMockStore() + mockStore := getMockStore(t) mockCacheProvider := getMockCacheProvider() cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider) require.NoError(t, err) @@ -65,7 +65,7 @@ func TestRoleStoreCache(t *testing.T) { }) t.Run("first call not cached, permanent delete all, and then not cached again", func(t *testing.T) { - mockStore := getMockStore() + mockStore := getMockStore(t) mockCacheProvider := getMockCacheProvider() cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider) require.NoError(t, err) diff --git a/server/channels/store/localcachelayer/scheme_layer_test.go b/server/channels/store/localcachelayer/scheme_layer_test.go index 98b520d7eca..1b526cc488a 100644 --- a/server/channels/store/localcachelayer/scheme_layer_test.go +++ b/server/channels/store/localcachelayer/scheme_layer_test.go @@ -22,7 +22,7 @@ func TestSchemeStoreCache(t *testing.T) { fakeScheme := model.Scheme{Id: "123", Name: "scheme-name"} t.Run("first call not cached, second cached and returning same data", func(t *testing.T) { - mockStore := getMockStore() + mockStore := getMockStore(t) mockCacheProvider := getMockCacheProvider() cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider) require.NoError(t, err) @@ -38,7 +38,7 @@ func TestSchemeStoreCache(t *testing.T) { }) t.Run("first call not cached, save, and then not cached again", func(t *testing.T) { - mockStore := getMockStore() + mockStore := getMockStore(t) mockCacheProvider := getMockCacheProvider() cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider) require.NoError(t, err) @@ -51,7 +51,7 @@ func TestSchemeStoreCache(t *testing.T) { }) t.Run("first call not cached, delete, and then not cached again", func(t *testing.T) { - mockStore := getMockStore() + mockStore := getMockStore(t) mockCacheProvider := getMockCacheProvider() cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider) require.NoError(t, err) @@ -64,7 +64,7 @@ func TestSchemeStoreCache(t *testing.T) { }) t.Run("first call not cached, permanent delete all, and then not cached again", func(t *testing.T) { - mockStore := getMockStore() + mockStore := getMockStore(t) mockCacheProvider := getMockCacheProvider() cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider) require.NoError(t, err) diff --git a/server/channels/store/localcachelayer/team_layer_test.go b/server/channels/store/localcachelayer/team_layer_test.go index 1bba1c7c26b..74b7ff7f822 100644 --- a/server/channels/store/localcachelayer/team_layer_test.go +++ b/server/channels/store/localcachelayer/team_layer_test.go @@ -22,7 +22,7 @@ func TestTeamStoreCache(t *testing.T) { fakeUserTeamIds := []string{"1", "2", "3"} t.Run("first call not cached, second cached and returning same data", func(t *testing.T) { - mockStore := getMockStore() + mockStore := getMockStore(t) mockCacheProvider := getMockCacheProvider() cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider) require.NoError(t, err) @@ -39,7 +39,7 @@ func TestTeamStoreCache(t *testing.T) { }) t.Run("first call not cached, second force not cached", func(t *testing.T) { - mockStore := getMockStore() + mockStore := getMockStore(t) mockCacheProvider := getMockCacheProvider() cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider) require.NoError(t, err) @@ -56,7 +56,7 @@ func TestTeamStoreCache(t *testing.T) { }) t.Run("first call not cached, invalidate, and then not cached again", func(t *testing.T) { - mockStore := getMockStore() + mockStore := getMockStore(t) mockCacheProvider := getMockCacheProvider() cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider) require.NoError(t, err) diff --git a/server/channels/store/localcachelayer/terms_of_service_layer_test.go b/server/channels/store/localcachelayer/terms_of_service_layer_test.go index a6e5ad12336..2f81a869838 100644 --- a/server/channels/store/localcachelayer/terms_of_service_layer_test.go +++ b/server/channels/store/localcachelayer/terms_of_service_layer_test.go @@ -23,7 +23,7 @@ func TestTermsOfServiceStoreTermsOfServiceCache(t *testing.T) { fakeTermsOfService := model.TermsOfService{Id: "123", CreateAt: 11111, UserId: "321", Text: "Terms of service test"} t.Run("first call by latest not cached, second cached and returning same data", func(t *testing.T) { - mockStore := getMockStore() + mockStore := getMockStore(t) mockCacheProvider := getMockCacheProvider() cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider) require.NoError(t, err) @@ -39,7 +39,7 @@ func TestTermsOfServiceStoreTermsOfServiceCache(t *testing.T) { }) t.Run("first call by id not cached, second cached and returning same data", func(t *testing.T) { - mockStore := getMockStore() + mockStore := getMockStore(t) mockCacheProvider := getMockCacheProvider() cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider) require.NoError(t, err) @@ -55,7 +55,7 @@ func TestTermsOfServiceStoreTermsOfServiceCache(t *testing.T) { }) t.Run("first call by id not cached, second force no cached", func(t *testing.T) { - mockStore := getMockStore() + mockStore := getMockStore(t) mockCacheProvider := getMockCacheProvider() cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider) require.NoError(t, err) @@ -67,7 +67,7 @@ func TestTermsOfServiceStoreTermsOfServiceCache(t *testing.T) { }) t.Run("first call latest not cached, second force no cached", func(t *testing.T) { - mockStore := getMockStore() + mockStore := getMockStore(t) mockCacheProvider := getMockCacheProvider() cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider) require.NoError(t, err) @@ -79,7 +79,7 @@ func TestTermsOfServiceStoreTermsOfServiceCache(t *testing.T) { }) t.Run("first call by id force no cached, second not cached, third cached", func(t *testing.T) { - mockStore := getMockStore() + mockStore := getMockStore(t) mockCacheProvider := getMockCacheProvider() cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider) require.NoError(t, err) @@ -93,7 +93,7 @@ func TestTermsOfServiceStoreTermsOfServiceCache(t *testing.T) { }) t.Run("first call latest force no cached, second not cached, third cached", func(t *testing.T) { - mockStore := getMockStore() + mockStore := getMockStore(t) mockCacheProvider := getMockCacheProvider() cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider) require.NoError(t, err) @@ -107,7 +107,7 @@ func TestTermsOfServiceStoreTermsOfServiceCache(t *testing.T) { }) t.Run("first call latest, second call by id cached", func(t *testing.T) { - mockStore := getMockStore() + mockStore := getMockStore(t) mockCacheProvider := getMockCacheProvider() cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider) require.NoError(t, err) @@ -119,7 +119,7 @@ func TestTermsOfServiceStoreTermsOfServiceCache(t *testing.T) { }) t.Run("first call by id not cached, save, and then not cached again", func(t *testing.T) { - mockStore := getMockStore() + mockStore := getMockStore(t) mockCacheProvider := getMockCacheProvider() cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider) require.NoError(t, err) @@ -132,7 +132,7 @@ func TestTermsOfServiceStoreTermsOfServiceCache(t *testing.T) { }) t.Run("first get latest not cached, save new, then get latest, returning different data", func(t *testing.T) { - mockStore := getMockStore() + mockStore := getMockStore(t) mockCacheProvider := getMockCacheProvider() cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider) require.NoError(t, err) diff --git a/server/channels/store/localcachelayer/user_layer_test.go b/server/channels/store/localcachelayer/user_layer_test.go index 19e74641295..a8b9d19e750 100644 --- a/server/channels/store/localcachelayer/user_layer_test.go +++ b/server/channels/store/localcachelayer/user_layer_test.go @@ -30,7 +30,7 @@ func TestUserStoreCache(t *testing.T) { }} t.Run("first call not cached, second cached and returning same data", func(t *testing.T) { - mockStore := getMockStore() + mockStore := getMockStore(t) mockCacheProvider := getMockCacheProvider() cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider) require.NoError(t, err) @@ -45,7 +45,7 @@ func TestUserStoreCache(t *testing.T) { }) t.Run("first call not cached, second force not cached", func(t *testing.T) { - mockStore := getMockStore() + mockStore := getMockStore(t) mockCacheProvider := getMockCacheProvider() cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider) require.NoError(t, err) @@ -60,7 +60,7 @@ func TestUserStoreCache(t *testing.T) { }) t.Run("first call not cached, invalidate, and then not cached again", func(t *testing.T) { - mockStore := getMockStore() + mockStore := getMockStore(t) mockCacheProvider := getMockCacheProvider() cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider) require.NoError(t, err) @@ -76,7 +76,7 @@ func TestUserStoreCache(t *testing.T) { }) t.Run("should always return a copy of the stored data", func(t *testing.T) { - mockStore := getMockStore() + mockStore := getMockStore(t) mockCacheProvider := getMockCacheProvider() cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider) require.NoError(t, err) @@ -126,7 +126,7 @@ func TestUserStoreProfilesInChannelCache(t *testing.T) { } t.Run("first call not cached, second cached and returning same data", func(t *testing.T) { - mockStore := getMockStore() + mockStore := getMockStore(t) mockCacheProvider := getMockCacheProvider() cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider) require.NoError(t, err) @@ -141,7 +141,7 @@ func TestUserStoreProfilesInChannelCache(t *testing.T) { }) t.Run("first call not cached, second force not cached", func(t *testing.T) { - mockStore := getMockStore() + mockStore := getMockStore(t) mockCacheProvider := getMockCacheProvider() cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider) require.NoError(t, err) @@ -156,7 +156,7 @@ func TestUserStoreProfilesInChannelCache(t *testing.T) { }) t.Run("first call not cached, invalidate by channel, and then not cached again", func(t *testing.T) { - mockStore := getMockStore() + mockStore := getMockStore(t) mockCacheProvider := getMockCacheProvider() cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider) require.NoError(t, err) @@ -173,7 +173,7 @@ func TestUserStoreProfilesInChannelCache(t *testing.T) { }) t.Run("first call not cached, invalidate by user, and then not cached again", func(t *testing.T) { - mockStore := getMockStore() + mockStore := getMockStore(t) mockCacheProvider := getMockCacheProvider() cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider) require.NoError(t, err) @@ -198,7 +198,7 @@ func TestUserStoreGetCache(t *testing.T) { AuthService: "authService", } t.Run("first call not cached, second cached and returning same data", func(t *testing.T) { - mockStore := getMockStore() + mockStore := getMockStore(t) mockCacheProvider := getMockCacheProvider() cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider) require.NoError(t, err) @@ -213,7 +213,7 @@ func TestUserStoreGetCache(t *testing.T) { }) t.Run("first call not cached, invalidate, and then not cached again", func(t *testing.T) { - mockStore := getMockStore() + mockStore := getMockStore(t) mockCacheProvider := getMockCacheProvider() cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider) require.NoError(t, err) @@ -230,7 +230,7 @@ func TestUserStoreGetCache(t *testing.T) { }) t.Run("should always return a copy of the stored data", func(t *testing.T) { - mockStore := getMockStore() + mockStore := getMockStore(t) mockCacheProvider := getMockCacheProvider() cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider) require.NoError(t, err) @@ -273,7 +273,7 @@ func TestUserStoreGetManyCache(t *testing.T) { AuthService: "authService", } t.Run("first call not cached, second cached and returning same data", func(t *testing.T) { - mockStore := getMockStore() + mockStore := getMockStore(t) mockCacheProvider := getMockCacheProvider() cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider) require.NoError(t, err) @@ -291,7 +291,7 @@ func TestUserStoreGetManyCache(t *testing.T) { }) t.Run("first call not cached, invalidate one user, and then check that one is cached and one is fetched from db", func(t *testing.T) { - mockStore := getMockStore() + mockStore := getMockStore(t) mockCacheProvider := getMockCacheProvider() cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider) require.NoError(t, err) diff --git a/server/channels/store/localcachelayer/webhook_layer_test.go b/server/channels/store/localcachelayer/webhook_layer_test.go index 608d4754fed..aeda6655beb 100644 --- a/server/channels/store/localcachelayer/webhook_layer_test.go +++ b/server/channels/store/localcachelayer/webhook_layer_test.go @@ -22,7 +22,7 @@ func TestWebhookStoreCache(t *testing.T) { fakeWebhook := model.IncomingWebhook{Id: "123"} t.Run("first call not cached, second cached and returning same data", func(t *testing.T) { - mockStore := getMockStore() + mockStore := getMockStore(t) mockCacheProvider := getMockCacheProvider() cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider) require.NoError(t, err) @@ -38,7 +38,7 @@ func TestWebhookStoreCache(t *testing.T) { }) t.Run("first call not cached, second force not cached", func(t *testing.T) { - mockStore := getMockStore() + mockStore := getMockStore(t) mockCacheProvider := getMockCacheProvider() cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider) require.NoError(t, err) @@ -50,7 +50,7 @@ func TestWebhookStoreCache(t *testing.T) { }) t.Run("first call not cached, invalidate, and then not cached again", func(t *testing.T) { - mockStore := getMockStore() + mockStore := getMockStore(t) mockCacheProvider := getMockCacheProvider() cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider) require.NoError(t, err) diff --git a/server/channels/store/opentracinglayer/opentracinglayer.go b/server/channels/store/opentracinglayer/opentracinglayer.go index 0ab2b248fdc..9e4f089f252 100644 --- a/server/channels/store/opentracinglayer/opentracinglayer.go +++ b/server/channels/store/opentracinglayer/opentracinglayer.go @@ -10,6 +10,7 @@ import ( "context" "github.com/mattermost/mattermost/server/public/model" + "github.com/mattermost/mattermost/server/public/shared/request" "github.com/mattermost/mattermost/server/v8/channels/store" "github.com/mattermost/mattermost/server/v8/platform/services/tracing" "github.com/opentracing/opentracing-go/ext" @@ -26,6 +27,7 @@ type OpenTracingLayer struct { CommandStore store.CommandStore CommandWebhookStore store.CommandWebhookStore ComplianceStore store.ComplianceStore + DesktopTokensStore store.DesktopTokensStore DraftStore store.DraftStore EmojiStore store.EmojiStore FileInfoStore store.FileInfoStore @@ -95,6 +97,10 @@ func (s *OpenTracingLayer) Compliance() store.ComplianceStore { return s.ComplianceStore } +func (s *OpenTracingLayer) DesktopTokens() store.DesktopTokensStore { + return s.DesktopTokensStore +} + func (s *OpenTracingLayer) Draft() store.DraftStore { return s.DraftStore } @@ -275,6 +281,11 @@ type OpenTracingLayerComplianceStore struct { Root *OpenTracingLayer } +type OpenTracingLayerDesktopTokensStore struct { + store.DesktopTokensStore + Root *OpenTracingLayer +} + type OpenTracingLayerDraftStore struct { store.DraftStore Root *OpenTracingLayer @@ -854,6 +865,24 @@ func (s *OpenTracingLayerChannelStore) Delete(channelID string, timestamp int64) return err } +func (s *OpenTracingLayerChannelStore) DeleteAllSidebarChannelForChannel(channelID string) error { + origCtx := s.Root.Store.Context() + span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.DeleteAllSidebarChannelForChannel") + s.Root.Store.SetContext(newCtx) + defer func() { + s.Root.Store.SetContext(origCtx) + }() + + defer span.Finish() + err := s.ChannelStore.DeleteAllSidebarChannelForChannel(channelID) + if err != nil { + span.LogFields(spanlog.Error(err)) + ext.Error.Set(span, true) + } + + return err +} + func (s *OpenTracingLayerChannelStore) DeleteSidebarCategory(categoryID string) error { origCtx := s.Root.Store.Context() span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.DeleteSidebarCategory") @@ -1106,6 +1135,24 @@ func (s *OpenTracingLayerChannelStore) GetByNames(team_id string, names []string return result, err } +func (s *OpenTracingLayerChannelStore) GetByNamesIncludeDeleted(team_id string, names []string, allowFromCache bool) ([]*model.Channel, error) { + origCtx := s.Root.Store.Context() + span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.GetByNamesIncludeDeleted") + s.Root.Store.SetContext(newCtx) + defer func() { + s.Root.Store.SetContext(origCtx) + }() + + defer span.Finish() + result, err := s.ChannelStore.GetByNamesIncludeDeleted(team_id, names, allowFromCache) + if err != nil { + span.LogFields(spanlog.Error(err)) + ext.Error.Set(span, true) + } + + return result, err +} + func (s *OpenTracingLayerChannelStore) GetChannelCounts(teamID string, userID string) (*model.ChannelCounts, error) { origCtx := s.Root.Store.Context() span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.GetChannelCounts") @@ -1124,7 +1171,7 @@ func (s *OpenTracingLayerChannelStore) GetChannelCounts(teamID string, userID st return result, err } -func (s *OpenTracingLayerChannelStore) GetChannelMembersForExport(userID string, teamID string) ([]*model.ChannelMemberForExport, error) { +func (s *OpenTracingLayerChannelStore) GetChannelMembersForExport(userID string, teamID string, includeArchivedChannel bool) ([]*model.ChannelMemberForExport, error) { origCtx := s.Root.Store.Context() span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.GetChannelMembersForExport") s.Root.Store.SetContext(newCtx) @@ -1133,7 +1180,7 @@ func (s *OpenTracingLayerChannelStore) GetChannelMembersForExport(userID string, }() defer span.Finish() - result, err := s.ChannelStore.GetChannelMembersForExport(userID, teamID) + result, err := s.ChannelStore.GetChannelMembersForExport(userID, teamID, includeArchivedChannel) if err != nil { span.LogFields(spanlog.Error(err)) ext.Error.Set(span, true) @@ -1322,6 +1369,24 @@ func (s *OpenTracingLayerChannelStore) GetChannelsWithTeamDataByIds(channelIds [ return result, err } +func (s *OpenTracingLayerChannelStore) GetChannelsWithUnreadsAndWithMentions(ctx context.Context, channelIDs []string, userID string, userNotifyProps model.StringMap) ([]string, []string, map[string]int64, error) { + origCtx := s.Root.Store.Context() + span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.GetChannelsWithUnreadsAndWithMentions") + s.Root.Store.SetContext(newCtx) + defer func() { + s.Root.Store.SetContext(origCtx) + }() + + defer span.Finish() + result, resultVar1, resultVar2, err := s.ChannelStore.GetChannelsWithUnreadsAndWithMentions(ctx, channelIDs, userID, userNotifyProps) + if err != nil { + span.LogFields(spanlog.Error(err)) + ext.Error.Set(span, true) + } + + return result, resultVar1, resultVar2, err +} + func (s *OpenTracingLayerChannelStore) GetDeleted(team_id string, offset int, limit int, userID string) (model.ChannelList, error) { origCtx := s.Root.Store.Context() span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.GetDeleted") @@ -3198,6 +3263,96 @@ func (s *OpenTracingLayerComplianceStore) Update(compliance *model.Compliance) ( return result, err } +func (s *OpenTracingLayerDesktopTokensStore) Delete(token string) error { + origCtx := s.Root.Store.Context() + span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "DesktopTokensStore.Delete") + s.Root.Store.SetContext(newCtx) + defer func() { + s.Root.Store.SetContext(origCtx) + }() + + defer span.Finish() + err := s.DesktopTokensStore.Delete(token) + if err != nil { + span.LogFields(spanlog.Error(err)) + ext.Error.Set(span, true) + } + + return err +} + +func (s *OpenTracingLayerDesktopTokensStore) DeleteByUserId(userId string) error { + origCtx := s.Root.Store.Context() + span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "DesktopTokensStore.DeleteByUserId") + s.Root.Store.SetContext(newCtx) + defer func() { + s.Root.Store.SetContext(origCtx) + }() + + defer span.Finish() + err := s.DesktopTokensStore.DeleteByUserId(userId) + if err != nil { + span.LogFields(spanlog.Error(err)) + ext.Error.Set(span, true) + } + + return err +} + +func (s *OpenTracingLayerDesktopTokensStore) DeleteOlderThan(minCreatedAt int64) error { + origCtx := s.Root.Store.Context() + span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "DesktopTokensStore.DeleteOlderThan") + s.Root.Store.SetContext(newCtx) + defer func() { + s.Root.Store.SetContext(origCtx) + }() + + defer span.Finish() + err := s.DesktopTokensStore.DeleteOlderThan(minCreatedAt) + if err != nil { + span.LogFields(spanlog.Error(err)) + ext.Error.Set(span, true) + } + + return err +} + +func (s *OpenTracingLayerDesktopTokensStore) GetUserId(token string, minCreatedAt int64) (*string, error) { + origCtx := s.Root.Store.Context() + span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "DesktopTokensStore.GetUserId") + s.Root.Store.SetContext(newCtx) + defer func() { + s.Root.Store.SetContext(origCtx) + }() + + defer span.Finish() + result, err := s.DesktopTokensStore.GetUserId(token, minCreatedAt) + if err != nil { + span.LogFields(spanlog.Error(err)) + ext.Error.Set(span, true) + } + + return result, err +} + +func (s *OpenTracingLayerDesktopTokensStore) Insert(token string, createAt int64, userId string) error { + origCtx := s.Root.Store.Context() + span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "DesktopTokensStore.Insert") + s.Root.Store.SetContext(newCtx) + defer func() { + s.Root.Store.SetContext(origCtx) + }() + + defer span.Finish() + err := s.DesktopTokensStore.Insert(token, createAt, userId) + if err != nil { + span.LogFields(spanlog.Error(err)) + ext.Error.Set(span, true) + } + + return err +} + func (s *OpenTracingLayerDraftStore) Delete(userID string, channelID string, rootID string) error { origCtx := s.Root.Store.Context() span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "DraftStore.Delete") @@ -3288,7 +3443,7 @@ func (s *OpenTracingLayerEmojiStore) Delete(emoji *model.Emoji, timestamp int64) return err } -func (s *OpenTracingLayerEmojiStore) Get(ctx context.Context, id string, allowFromCache bool) (*model.Emoji, error) { +func (s *OpenTracingLayerEmojiStore) Get(ctx request.CTX, id string, allowFromCache bool) (*model.Emoji, error) { origCtx := s.Root.Store.Context() span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "EmojiStore.Get") s.Root.Store.SetContext(newCtx) @@ -3306,7 +3461,7 @@ func (s *OpenTracingLayerEmojiStore) Get(ctx context.Context, id string, allowFr return result, err } -func (s *OpenTracingLayerEmojiStore) GetByName(ctx context.Context, name string, allowFromCache bool) (*model.Emoji, error) { +func (s *OpenTracingLayerEmojiStore) GetByName(ctx request.CTX, name string, allowFromCache bool) (*model.Emoji, error) { origCtx := s.Root.Store.Context() span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "EmojiStore.GetByName") s.Root.Store.SetContext(newCtx) @@ -3342,7 +3497,7 @@ func (s *OpenTracingLayerEmojiStore) GetList(offset int, limit int, sort string) return result, err } -func (s *OpenTracingLayerEmojiStore) GetMultipleByName(ctx context.Context, names []string) ([]*model.Emoji, error) { +func (s *OpenTracingLayerEmojiStore) GetMultipleByName(ctx request.CTX, names []string) ([]*model.Emoji, error) { origCtx := s.Root.Store.Context() span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "EmojiStore.GetMultipleByName") s.Root.Store.SetContext(newCtx) @@ -4790,7 +4945,7 @@ func (s *OpenTracingLayerJobStore) Delete(id string) (string, error) { return result, err } -func (s *OpenTracingLayerJobStore) Get(id string) (*model.Job, error) { +func (s *OpenTracingLayerJobStore) Get(c *request.Context, id string) (*model.Job, error) { origCtx := s.Root.Store.Context() span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "JobStore.Get") s.Root.Store.SetContext(newCtx) @@ -4799,7 +4954,7 @@ func (s *OpenTracingLayerJobStore) Get(id string) (*model.Job, error) { }() defer span.Finish() - result, err := s.JobStore.Get(id) + result, err := s.JobStore.Get(c, id) if err != nil { span.LogFields(spanlog.Error(err)) ext.Error.Set(span, true) @@ -4808,7 +4963,7 @@ func (s *OpenTracingLayerJobStore) Get(id string) (*model.Job, error) { return result, err } -func (s *OpenTracingLayerJobStore) GetAllByStatus(status string) ([]*model.Job, error) { +func (s *OpenTracingLayerJobStore) GetAllByStatus(c *request.Context, status string) ([]*model.Job, error) { origCtx := s.Root.Store.Context() span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "JobStore.GetAllByStatus") s.Root.Store.SetContext(newCtx) @@ -4817,7 +4972,7 @@ func (s *OpenTracingLayerJobStore) GetAllByStatus(status string) ([]*model.Job, }() defer span.Finish() - result, err := s.JobStore.GetAllByStatus(status) + result, err := s.JobStore.GetAllByStatus(c, status) if err != nil { span.LogFields(spanlog.Error(err)) ext.Error.Set(span, true) @@ -4826,7 +4981,7 @@ func (s *OpenTracingLayerJobStore) GetAllByStatus(status string) ([]*model.Job, return result, err } -func (s *OpenTracingLayerJobStore) GetAllByType(jobType string) ([]*model.Job, error) { +func (s *OpenTracingLayerJobStore) GetAllByType(c *request.Context, jobType string) ([]*model.Job, error) { origCtx := s.Root.Store.Context() span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "JobStore.GetAllByType") s.Root.Store.SetContext(newCtx) @@ -4835,7 +4990,7 @@ func (s *OpenTracingLayerJobStore) GetAllByType(jobType string) ([]*model.Job, e }() defer span.Finish() - result, err := s.JobStore.GetAllByType(jobType) + result, err := s.JobStore.GetAllByType(c, jobType) if err != nil { span.LogFields(spanlog.Error(err)) ext.Error.Set(span, true) @@ -4844,7 +4999,7 @@ func (s *OpenTracingLayerJobStore) GetAllByType(jobType string) ([]*model.Job, e return result, err } -func (s *OpenTracingLayerJobStore) GetAllByTypeAndStatus(jobType string, status string) ([]*model.Job, error) { +func (s *OpenTracingLayerJobStore) GetAllByTypeAndStatus(c *request.Context, jobType string, status string) ([]*model.Job, error) { origCtx := s.Root.Store.Context() span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "JobStore.GetAllByTypeAndStatus") s.Root.Store.SetContext(newCtx) @@ -4853,7 +5008,7 @@ func (s *OpenTracingLayerJobStore) GetAllByTypeAndStatus(jobType string, status }() defer span.Finish() - result, err := s.JobStore.GetAllByTypeAndStatus(jobType, status) + result, err := s.JobStore.GetAllByTypeAndStatus(c, jobType, status) if err != nil { span.LogFields(spanlog.Error(err)) ext.Error.Set(span, true) @@ -4862,7 +5017,7 @@ func (s *OpenTracingLayerJobStore) GetAllByTypeAndStatus(jobType string, status return result, err } -func (s *OpenTracingLayerJobStore) GetAllByTypePage(jobType string, offset int, limit int) ([]*model.Job, error) { +func (s *OpenTracingLayerJobStore) GetAllByTypePage(c *request.Context, jobType string, offset int, limit int) ([]*model.Job, error) { origCtx := s.Root.Store.Context() span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "JobStore.GetAllByTypePage") s.Root.Store.SetContext(newCtx) @@ -4871,7 +5026,7 @@ func (s *OpenTracingLayerJobStore) GetAllByTypePage(jobType string, offset int, }() defer span.Finish() - result, err := s.JobStore.GetAllByTypePage(jobType, offset, limit) + result, err := s.JobStore.GetAllByTypePage(c, jobType, offset, limit) if err != nil { span.LogFields(spanlog.Error(err)) ext.Error.Set(span, true) @@ -4880,7 +5035,7 @@ func (s *OpenTracingLayerJobStore) GetAllByTypePage(jobType string, offset int, return result, err } -func (s *OpenTracingLayerJobStore) GetAllByTypesPage(jobTypes []string, offset int, limit int) ([]*model.Job, error) { +func (s *OpenTracingLayerJobStore) GetAllByTypesPage(c *request.Context, jobTypes []string, offset int, limit int) ([]*model.Job, error) { origCtx := s.Root.Store.Context() span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "JobStore.GetAllByTypesPage") s.Root.Store.SetContext(newCtx) @@ -4889,25 +5044,7 @@ func (s *OpenTracingLayerJobStore) GetAllByTypesPage(jobTypes []string, offset i }() defer span.Finish() - result, err := s.JobStore.GetAllByTypesPage(jobTypes, offset, limit) - if err != nil { - span.LogFields(spanlog.Error(err)) - ext.Error.Set(span, true) - } - - return result, err -} - -func (s *OpenTracingLayerJobStore) GetAllPage(offset int, limit int) ([]*model.Job, error) { - origCtx := s.Root.Store.Context() - span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "JobStore.GetAllPage") - s.Root.Store.SetContext(newCtx) - defer func() { - s.Root.Store.SetContext(origCtx) - }() - - defer span.Finish() - result, err := s.JobStore.GetAllPage(offset, limit) + result, err := s.JobStore.GetAllByTypesPage(c, jobTypes, offset, limit) if err != nil { span.LogFields(spanlog.Error(err)) ext.Error.Set(span, true) @@ -5060,7 +5197,7 @@ func (s *OpenTracingLayerJobStore) UpdateStatusOptimistically(id string, current return result, err } -func (s *OpenTracingLayerLicenseStore) Get(id string) (*model.LicenseRecord, error) { +func (s *OpenTracingLayerLicenseStore) Get(ctx context.Context, id string) (*model.LicenseRecord, error) { origCtx := s.Root.Store.Context() span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "LicenseStore.Get") s.Root.Store.SetContext(newCtx) @@ -5069,7 +5206,7 @@ func (s *OpenTracingLayerLicenseStore) Get(id string) (*model.LicenseRecord, err }() defer span.Finish() - result, err := s.LicenseStore.Get(id) + result, err := s.LicenseStore.Get(ctx, id) if err != nil { span.LogFields(spanlog.Error(err)) ext.Error.Set(span, true) @@ -5096,7 +5233,7 @@ func (s *OpenTracingLayerLicenseStore) GetAll() ([]*model.LicenseRecord, error) return result, err } -func (s *OpenTracingLayerLicenseStore) Save(license *model.LicenseRecord) (*model.LicenseRecord, error) { +func (s *OpenTracingLayerLicenseStore) Save(license *model.LicenseRecord) error { origCtx := s.Root.Store.Context() span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "LicenseStore.Save") s.Root.Store.SetContext(newCtx) @@ -5105,13 +5242,13 @@ func (s *OpenTracingLayerLicenseStore) Save(license *model.LicenseRecord) (*mode }() defer span.Finish() - result, err := s.LicenseStore.Save(license) + err := s.LicenseStore.Save(license) if err != nil { span.LogFields(spanlog.Error(err)) ext.Error.Set(span, true) } - return result, err + return err } func (s *OpenTracingLayerLinkMetadataStore) Get(url string, timestamp int64) (*model.LinkMetadata, error) { @@ -5865,24 +6002,6 @@ func (s *OpenTracingLayerPostStore) Delete(postID string, timestamp int64, delet return err } -func (s *OpenTracingLayerPostStore) DeleteOrphanedRows(limit int) (int64, error) { - origCtx := s.Root.Store.Context() - span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PostStore.DeleteOrphanedRows") - s.Root.Store.SetContext(newCtx) - defer func() { - s.Root.Store.SetContext(origCtx) - }() - - defer span.Finish() - result, err := s.PostStore.DeleteOrphanedRows(limit) - if err != nil { - span.LogFields(spanlog.Error(err)) - ext.Error.Set(span, true) - } - - return result, err -} - func (s *OpenTracingLayerPostStore) Get(ctx context.Context, id string, opts model.GetPostsOptions, userID string, sanitizeOptions map[string]bool) (*model.PostList, error) { origCtx := s.Root.Store.Context() span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PostStore.Get") @@ -6079,7 +6198,7 @@ func (s *OpenTracingLayerPostStore) GetOldestEntityCreationTime() (int64, error) return result, err } -func (s *OpenTracingLayerPostStore) GetParentsForExportAfter(limit int, afterID string) ([]*model.PostForExport, error) { +func (s *OpenTracingLayerPostStore) GetParentsForExportAfter(limit int, afterID string, includeArchivedChannels bool) ([]*model.PostForExport, error) { origCtx := s.Root.Store.Context() span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PostStore.GetParentsForExportAfter") s.Root.Store.SetContext(newCtx) @@ -6088,7 +6207,7 @@ func (s *OpenTracingLayerPostStore) GetParentsForExportAfter(limit int, afterID }() defer span.Finish() - result, err := s.PostStore.GetParentsForExportAfter(limit, afterID) + result, err := s.PostStore.GetParentsForExportAfter(limit, afterID, includeArchivedChannels) if err != nil { span.LogFields(spanlog.Error(err)) ext.Error.Set(span, true) @@ -7208,22 +7327,22 @@ func (s *OpenTracingLayerReactionStore) DeleteAllWithEmojiName(emojiName string) return err } -func (s *OpenTracingLayerReactionStore) DeleteOrphanedRows(limit int) (int64, error) { +func (s *OpenTracingLayerReactionStore) DeleteOrphanedRowsByIds(r *model.RetentionIdsForDeletion) error { origCtx := s.Root.Store.Context() - span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ReactionStore.DeleteOrphanedRows") + span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ReactionStore.DeleteOrphanedRowsByIds") s.Root.Store.SetContext(newCtx) defer func() { s.Root.Store.SetContext(origCtx) }() defer span.Finish() - result, err := s.ReactionStore.DeleteOrphanedRows(limit) + err := s.ReactionStore.DeleteOrphanedRowsByIds(r) if err != nil { span.LogFields(spanlog.Error(err)) ext.Error.Set(span, true) } - return result, err + return err } func (s *OpenTracingLayerReactionStore) GetForPost(postID string, allowFromCache bool) ([]*model.Reaction, error) { @@ -7280,6 +7399,24 @@ func (s *OpenTracingLayerReactionStore) PermanentDeleteBatch(endTime int64, limi return result, err } +func (s *OpenTracingLayerReactionStore) PermanentDeleteByUser(userID string) error { + origCtx := s.Root.Store.Context() + span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ReactionStore.PermanentDeleteByUser") + s.Root.Store.SetContext(newCtx) + defer func() { + s.Root.Store.SetContext(origCtx) + }() + + defer span.Finish() + err := s.ReactionStore.PermanentDeleteByUser(userID) + if err != nil { + span.LogFields(spanlog.Error(err)) + ext.Error.Set(span, true) + } + + return err +} + func (s *OpenTracingLayerReactionStore) Save(reaction *model.Reaction) (*model.Reaction, error) { origCtx := s.Root.Store.Context() span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ReactionStore.Save") @@ -7622,6 +7759,24 @@ func (s *OpenTracingLayerRetentionPolicyStore) GetCount() (int64, error) { return result, err } +func (s *OpenTracingLayerRetentionPolicyStore) GetIdsForDeletionByTableName(tableName string, limit int) ([]*model.RetentionIdsForDeletion, error) { + origCtx := s.Root.Store.Context() + span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "RetentionPolicyStore.GetIdsForDeletionByTableName") + s.Root.Store.SetContext(newCtx) + defer func() { + s.Root.Store.SetContext(origCtx) + }() + + defer span.Finish() + result, err := s.RetentionPolicyStore.GetIdsForDeletionByTableName(tableName, limit) + if err != nil { + span.LogFields(spanlog.Error(err)) + ext.Error.Set(span, true) + } + + return result, err +} + func (s *OpenTracingLayerRetentionPolicyStore) GetTeamPoliciesCountForUser(userID string) (int64, error) { origCtx := s.Root.Store.Context() span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "RetentionPolicyStore.GetTeamPoliciesCountForUser") @@ -9417,6 +9572,24 @@ func (s *OpenTracingLayerTeamStore) GetChannelUnreadsForTeam(teamID string, user return result, err } +func (s *OpenTracingLayerTeamStore) GetCommonTeamIDsForMultipleUsers(userIDs []string) ([]string, error) { + origCtx := s.Root.Store.Context() + span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "TeamStore.GetCommonTeamIDsForMultipleUsers") + s.Root.Store.SetContext(newCtx) + defer func() { + s.Root.Store.SetContext(origCtx) + }() + + defer span.Finish() + result, err := s.TeamStore.GetCommonTeamIDsForMultipleUsers(userIDs) + if err != nil { + span.LogFields(spanlog.Error(err)) + ext.Error.Set(span, true) + } + + return result, err +} + func (s *OpenTracingLayerTeamStore) GetCommonTeamIDsForTwoUsers(userID string, otherUserID string) ([]string, error) { origCtx := s.Root.Store.Context() span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "TeamStore.GetCommonTeamIDsForTwoUsers") @@ -11130,6 +11303,24 @@ func (s *OpenTracingLayerUserStore) GetByEmail(email string) (*model.User, error return result, err } +func (s *OpenTracingLayerUserStore) GetByRemoteID(remoteID string) (*model.User, error) { + origCtx := s.Root.Store.Context() + span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UserStore.GetByRemoteID") + s.Root.Store.SetContext(newCtx) + defer func() { + s.Root.Store.SetContext(origCtx) + }() + + defer span.Finish() + result, err := s.UserStore.GetByRemoteID(remoteID) + if err != nil { + span.LogFields(spanlog.Error(err)) + ext.Error.Set(span, true) + } + + return result, err +} + func (s *OpenTracingLayerUserStore) GetByUsername(username string) (*model.User, error) { origCtx := s.Root.Store.Context() span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UserStore.GetByUsername") @@ -12831,6 +13022,7 @@ func New(childStore store.Store, ctx context.Context) *OpenTracingLayer { newStore.CommandStore = &OpenTracingLayerCommandStore{CommandStore: childStore.Command(), Root: &newStore} newStore.CommandWebhookStore = &OpenTracingLayerCommandWebhookStore{CommandWebhookStore: childStore.CommandWebhook(), Root: &newStore} newStore.ComplianceStore = &OpenTracingLayerComplianceStore{ComplianceStore: childStore.Compliance(), Root: &newStore} + newStore.DesktopTokensStore = &OpenTracingLayerDesktopTokensStore{DesktopTokensStore: childStore.DesktopTokens(), Root: &newStore} newStore.DraftStore = &OpenTracingLayerDraftStore{DraftStore: childStore.Draft(), Root: &newStore} newStore.EmojiStore = &OpenTracingLayerEmojiStore{EmojiStore: childStore.Emoji(), Root: &newStore} newStore.FileInfoStore = &OpenTracingLayerFileInfoStore{FileInfoStore: childStore.FileInfo(), Root: &newStore} diff --git a/server/channels/store/retrylayer/retrylayer.go b/server/channels/store/retrylayer/retrylayer.go index 3f965768e91..6811f4c00e0 100644 --- a/server/channels/store/retrylayer/retrylayer.go +++ b/server/channels/store/retrylayer/retrylayer.go @@ -12,9 +12,11 @@ import ( "github.com/go-sql-driver/mysql" "github.com/lib/pq" - "github.com/mattermost/mattermost/server/public/model" - "github.com/mattermost/mattermost/server/v8/channels/store" "github.com/pkg/errors" + + "github.com/mattermost/mattermost/server/public/model" + "github.com/mattermost/mattermost/server/public/shared/request" + "github.com/mattermost/mattermost/server/v8/channels/store" ) const mySQLDeadlockCode = uint16(1213) @@ -29,6 +31,7 @@ type RetryLayer struct { CommandStore store.CommandStore CommandWebhookStore store.CommandWebhookStore ComplianceStore store.ComplianceStore + DesktopTokensStore store.DesktopTokensStore DraftStore store.DraftStore EmojiStore store.EmojiStore FileInfoStore store.FileInfoStore @@ -98,6 +101,10 @@ func (s *RetryLayer) Compliance() store.ComplianceStore { return s.ComplianceStore } +func (s *RetryLayer) DesktopTokens() store.DesktopTokensStore { + return s.DesktopTokensStore +} + func (s *RetryLayer) Draft() store.DraftStore { return s.DraftStore } @@ -278,6 +285,11 @@ type RetryLayerComplianceStore struct { Root *RetryLayer } +type RetryLayerDesktopTokensStore struct { + store.DesktopTokensStore + Root *RetryLayer +} + type RetryLayerDraftStore struct { store.DraftStore Root *RetryLayer @@ -922,6 +934,27 @@ func (s *RetryLayerChannelStore) Delete(channelID string, timestamp int64) error } +func (s *RetryLayerChannelStore) DeleteAllSidebarChannelForChannel(channelID string) error { + + tries := 0 + for { + err := s.ChannelStore.DeleteAllSidebarChannelForChannel(channelID) + if err == nil { + return nil + } + if !isRepeatableError(err) { + return err + } + tries++ + if tries >= 3 { + err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures") + return err + } + timepkg.Sleep(100 * timepkg.Millisecond) + } + +} + func (s *RetryLayerChannelStore) DeleteSidebarCategory(categoryID string) error { tries := 0 @@ -1216,6 +1249,27 @@ func (s *RetryLayerChannelStore) GetByNames(team_id string, names []string, allo } +func (s *RetryLayerChannelStore) GetByNamesIncludeDeleted(team_id string, names []string, allowFromCache bool) ([]*model.Channel, error) { + + tries := 0 + for { + result, err := s.ChannelStore.GetByNamesIncludeDeleted(team_id, names, allowFromCache) + if err == nil { + return result, nil + } + if !isRepeatableError(err) { + return result, err + } + tries++ + if tries >= 3 { + err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures") + return result, err + } + timepkg.Sleep(100 * timepkg.Millisecond) + } + +} + func (s *RetryLayerChannelStore) GetChannelCounts(teamID string, userID string) (*model.ChannelCounts, error) { tries := 0 @@ -1237,11 +1291,11 @@ func (s *RetryLayerChannelStore) GetChannelCounts(teamID string, userID string) } -func (s *RetryLayerChannelStore) GetChannelMembersForExport(userID string, teamID string) ([]*model.ChannelMemberForExport, error) { +func (s *RetryLayerChannelStore) GetChannelMembersForExport(userID string, teamID string, includeArchivedChannel bool) ([]*model.ChannelMemberForExport, error) { tries := 0 for { - result, err := s.ChannelStore.GetChannelMembersForExport(userID, teamID) + result, err := s.ChannelStore.GetChannelMembersForExport(userID, teamID, includeArchivedChannel) if err == nil { return result, nil } @@ -1468,6 +1522,27 @@ func (s *RetryLayerChannelStore) GetChannelsWithTeamDataByIds(channelIds []strin } +func (s *RetryLayerChannelStore) GetChannelsWithUnreadsAndWithMentions(ctx context.Context, channelIDs []string, userID string, userNotifyProps model.StringMap) ([]string, []string, map[string]int64, error) { + + tries := 0 + for { + result, resultVar1, resultVar2, err := s.ChannelStore.GetChannelsWithUnreadsAndWithMentions(ctx, channelIDs, userID, userNotifyProps) + if err == nil { + return result, resultVar1, resultVar2, nil + } + if !isRepeatableError(err) { + return result, resultVar1, resultVar2, err + } + tries++ + if tries >= 3 { + err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures") + return result, resultVar1, resultVar2, err + } + timepkg.Sleep(100 * timepkg.Millisecond) + } + +} + func (s *RetryLayerChannelStore) GetDeleted(team_id string, offset int, limit int, userID string) (model.ChannelList, error) { tries := 0 @@ -3565,6 +3640,111 @@ func (s *RetryLayerComplianceStore) Update(compliance *model.Compliance) (*model } +func (s *RetryLayerDesktopTokensStore) Delete(token string) error { + + tries := 0 + for { + err := s.DesktopTokensStore.Delete(token) + if err == nil { + return nil + } + if !isRepeatableError(err) { + return err + } + tries++ + if tries >= 3 { + err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures") + return err + } + timepkg.Sleep(100 * timepkg.Millisecond) + } + +} + +func (s *RetryLayerDesktopTokensStore) DeleteByUserId(userId string) error { + + tries := 0 + for { + err := s.DesktopTokensStore.DeleteByUserId(userId) + if err == nil { + return nil + } + if !isRepeatableError(err) { + return err + } + tries++ + if tries >= 3 { + err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures") + return err + } + timepkg.Sleep(100 * timepkg.Millisecond) + } + +} + +func (s *RetryLayerDesktopTokensStore) DeleteOlderThan(minCreatedAt int64) error { + + tries := 0 + for { + err := s.DesktopTokensStore.DeleteOlderThan(minCreatedAt) + if err == nil { + return nil + } + if !isRepeatableError(err) { + return err + } + tries++ + if tries >= 3 { + err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures") + return err + } + timepkg.Sleep(100 * timepkg.Millisecond) + } + +} + +func (s *RetryLayerDesktopTokensStore) GetUserId(token string, minCreatedAt int64) (*string, error) { + + tries := 0 + for { + result, err := s.DesktopTokensStore.GetUserId(token, minCreatedAt) + if err == nil { + return result, nil + } + if !isRepeatableError(err) { + return result, err + } + tries++ + if tries >= 3 { + err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures") + return result, err + } + timepkg.Sleep(100 * timepkg.Millisecond) + } + +} + +func (s *RetryLayerDesktopTokensStore) Insert(token string, createAt int64, userId string) error { + + tries := 0 + for { + err := s.DesktopTokensStore.Insert(token, createAt, userId) + if err == nil { + return nil + } + if !isRepeatableError(err) { + return err + } + tries++ + if tries >= 3 { + err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures") + return err + } + timepkg.Sleep(100 * timepkg.Millisecond) + } + +} + func (s *RetryLayerDraftStore) Delete(userID string, channelID string, rootID string) error { tries := 0 @@ -3670,7 +3850,7 @@ func (s *RetryLayerEmojiStore) Delete(emoji *model.Emoji, timestamp int64) error } -func (s *RetryLayerEmojiStore) Get(ctx context.Context, id string, allowFromCache bool) (*model.Emoji, error) { +func (s *RetryLayerEmojiStore) Get(ctx request.CTX, id string, allowFromCache bool) (*model.Emoji, error) { tries := 0 for { @@ -3691,7 +3871,7 @@ func (s *RetryLayerEmojiStore) Get(ctx context.Context, id string, allowFromCach } -func (s *RetryLayerEmojiStore) GetByName(ctx context.Context, name string, allowFromCache bool) (*model.Emoji, error) { +func (s *RetryLayerEmojiStore) GetByName(ctx request.CTX, name string, allowFromCache bool) (*model.Emoji, error) { tries := 0 for { @@ -3733,7 +3913,7 @@ func (s *RetryLayerEmojiStore) GetList(offset int, limit int, sort string) ([]*m } -func (s *RetryLayerEmojiStore) GetMultipleByName(ctx context.Context, names []string) ([]*model.Emoji, error) { +func (s *RetryLayerEmojiStore) GetMultipleByName(ctx request.CTX, names []string) ([]*model.Emoji, error) { tries := 0 for { @@ -5404,11 +5584,11 @@ func (s *RetryLayerJobStore) Delete(id string) (string, error) { } -func (s *RetryLayerJobStore) Get(id string) (*model.Job, error) { +func (s *RetryLayerJobStore) Get(c *request.Context, id string) (*model.Job, error) { tries := 0 for { - result, err := s.JobStore.Get(id) + result, err := s.JobStore.Get(c, id) if err == nil { return result, nil } @@ -5425,11 +5605,11 @@ func (s *RetryLayerJobStore) Get(id string) (*model.Job, error) { } -func (s *RetryLayerJobStore) GetAllByStatus(status string) ([]*model.Job, error) { +func (s *RetryLayerJobStore) GetAllByStatus(c *request.Context, status string) ([]*model.Job, error) { tries := 0 for { - result, err := s.JobStore.GetAllByStatus(status) + result, err := s.JobStore.GetAllByStatus(c, status) if err == nil { return result, nil } @@ -5446,11 +5626,11 @@ func (s *RetryLayerJobStore) GetAllByStatus(status string) ([]*model.Job, error) } -func (s *RetryLayerJobStore) GetAllByType(jobType string) ([]*model.Job, error) { +func (s *RetryLayerJobStore) GetAllByType(c *request.Context, jobType string) ([]*model.Job, error) { tries := 0 for { - result, err := s.JobStore.GetAllByType(jobType) + result, err := s.JobStore.GetAllByType(c, jobType) if err == nil { return result, nil } @@ -5467,11 +5647,11 @@ func (s *RetryLayerJobStore) GetAllByType(jobType string) ([]*model.Job, error) } -func (s *RetryLayerJobStore) GetAllByTypeAndStatus(jobType string, status string) ([]*model.Job, error) { +func (s *RetryLayerJobStore) GetAllByTypeAndStatus(c *request.Context, jobType string, status string) ([]*model.Job, error) { tries := 0 for { - result, err := s.JobStore.GetAllByTypeAndStatus(jobType, status) + result, err := s.JobStore.GetAllByTypeAndStatus(c, jobType, status) if err == nil { return result, nil } @@ -5488,11 +5668,11 @@ func (s *RetryLayerJobStore) GetAllByTypeAndStatus(jobType string, status string } -func (s *RetryLayerJobStore) GetAllByTypePage(jobType string, offset int, limit int) ([]*model.Job, error) { +func (s *RetryLayerJobStore) GetAllByTypePage(c *request.Context, jobType string, offset int, limit int) ([]*model.Job, error) { tries := 0 for { - result, err := s.JobStore.GetAllByTypePage(jobType, offset, limit) + result, err := s.JobStore.GetAllByTypePage(c, jobType, offset, limit) if err == nil { return result, nil } @@ -5509,32 +5689,11 @@ func (s *RetryLayerJobStore) GetAllByTypePage(jobType string, offset int, limit } -func (s *RetryLayerJobStore) GetAllByTypesPage(jobTypes []string, offset int, limit int) ([]*model.Job, error) { +func (s *RetryLayerJobStore) GetAllByTypesPage(c *request.Context, jobTypes []string, offset int, limit int) ([]*model.Job, error) { tries := 0 for { - result, err := s.JobStore.GetAllByTypesPage(jobTypes, offset, limit) - if err == nil { - return result, nil - } - if !isRepeatableError(err) { - return result, err - } - tries++ - if tries >= 3 { - err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures") - return result, err - } - timepkg.Sleep(100 * timepkg.Millisecond) - } - -} - -func (s *RetryLayerJobStore) GetAllPage(offset int, limit int) ([]*model.Job, error) { - - tries := 0 - for { - result, err := s.JobStore.GetAllPage(offset, limit) + result, err := s.JobStore.GetAllByTypesPage(c, jobTypes, offset, limit) if err == nil { return result, nil } @@ -5719,11 +5878,11 @@ func (s *RetryLayerJobStore) UpdateStatusOptimistically(id string, currentStatus } -func (s *RetryLayerLicenseStore) Get(id string) (*model.LicenseRecord, error) { +func (s *RetryLayerLicenseStore) Get(ctx context.Context, id string) (*model.LicenseRecord, error) { tries := 0 for { - result, err := s.LicenseStore.Get(id) + result, err := s.LicenseStore.Get(ctx, id) if err == nil { return result, nil } @@ -5761,21 +5920,21 @@ func (s *RetryLayerLicenseStore) GetAll() ([]*model.LicenseRecord, error) { } -func (s *RetryLayerLicenseStore) Save(license *model.LicenseRecord) (*model.LicenseRecord, error) { +func (s *RetryLayerLicenseStore) Save(license *model.LicenseRecord) error { tries := 0 for { - result, err := s.LicenseStore.Save(license) + err := s.LicenseStore.Save(license) if err == nil { - return result, nil + return nil } if !isRepeatableError(err) { - return result, err + return err } tries++ if tries >= 3 { err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures") - return result, err + return err } timepkg.Sleep(100 * timepkg.Millisecond) } @@ -6649,27 +6808,6 @@ func (s *RetryLayerPostStore) Delete(postID string, timestamp int64, deleteByID } -func (s *RetryLayerPostStore) DeleteOrphanedRows(limit int) (int64, error) { - - tries := 0 - for { - result, err := s.PostStore.DeleteOrphanedRows(limit) - if err == nil { - return result, nil - } - if !isRepeatableError(err) { - return result, err - } - tries++ - if tries >= 3 { - err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures") - return result, err - } - timepkg.Sleep(100 * timepkg.Millisecond) - } - -} - func (s *RetryLayerPostStore) Get(ctx context.Context, id string, opts model.GetPostsOptions, userID string, sanitizeOptions map[string]bool) (*model.PostList, error) { tries := 0 @@ -6871,11 +7009,11 @@ func (s *RetryLayerPostStore) GetOldestEntityCreationTime() (int64, error) { } -func (s *RetryLayerPostStore) GetParentsForExportAfter(limit int, afterID string) ([]*model.PostForExport, error) { +func (s *RetryLayerPostStore) GetParentsForExportAfter(limit int, afterID string, includeArchivedChannels bool) ([]*model.PostForExport, error) { tries := 0 for { - result, err := s.PostStore.GetParentsForExportAfter(limit, afterID) + result, err := s.PostStore.GetParentsForExportAfter(limit, afterID, includeArchivedChannels) if err == nil { return result, nil } @@ -8179,21 +8317,21 @@ func (s *RetryLayerReactionStore) DeleteAllWithEmojiName(emojiName string) error } -func (s *RetryLayerReactionStore) DeleteOrphanedRows(limit int) (int64, error) { +func (s *RetryLayerReactionStore) DeleteOrphanedRowsByIds(r *model.RetentionIdsForDeletion) error { tries := 0 for { - result, err := s.ReactionStore.DeleteOrphanedRows(limit) + err := s.ReactionStore.DeleteOrphanedRowsByIds(r) if err == nil { - return result, nil + return nil } if !isRepeatableError(err) { - return result, err + return err } tries++ if tries >= 3 { err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures") - return result, err + return err } timepkg.Sleep(100 * timepkg.Millisecond) } @@ -8263,6 +8401,27 @@ func (s *RetryLayerReactionStore) PermanentDeleteBatch(endTime int64, limit int6 } +func (s *RetryLayerReactionStore) PermanentDeleteByUser(userID string) error { + + tries := 0 + for { + err := s.ReactionStore.PermanentDeleteByUser(userID) + if err == nil { + return nil + } + if !isRepeatableError(err) { + return err + } + tries++ + if tries >= 3 { + err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures") + return err + } + timepkg.Sleep(100 * timepkg.Millisecond) + } + +} + func (s *RetryLayerReactionStore) Save(reaction *model.Reaction) (*model.Reaction, error) { tries := 0 @@ -8662,6 +8821,27 @@ func (s *RetryLayerRetentionPolicyStore) GetCount() (int64, error) { } +func (s *RetryLayerRetentionPolicyStore) GetIdsForDeletionByTableName(tableName string, limit int) ([]*model.RetentionIdsForDeletion, error) { + + tries := 0 + for { + result, err := s.RetentionPolicyStore.GetIdsForDeletionByTableName(tableName, limit) + if err == nil { + return result, nil + } + if !isRepeatableError(err) { + return result, err + } + tries++ + if tries >= 3 { + err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures") + return result, err + } + timepkg.Sleep(100 * timepkg.Millisecond) + } + +} + func (s *RetryLayerRetentionPolicyStore) GetTeamPoliciesCountForUser(userID string) (int64, error) { tries := 0 @@ -10747,6 +10927,27 @@ func (s *RetryLayerTeamStore) GetChannelUnreadsForTeam(teamID string, userID str } +func (s *RetryLayerTeamStore) GetCommonTeamIDsForMultipleUsers(userIDs []string) ([]string, error) { + + tries := 0 + for { + result, err := s.TeamStore.GetCommonTeamIDsForMultipleUsers(userIDs) + if err == nil { + return result, nil + } + if !isRepeatableError(err) { + return result, err + } + tries++ + if tries >= 3 { + err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures") + return result, err + } + timepkg.Sleep(100 * timepkg.Millisecond) + } + +} + func (s *RetryLayerTeamStore) GetCommonTeamIDsForTwoUsers(userID string, otherUserID string) ([]string, error) { tries := 0 @@ -12718,6 +12919,27 @@ func (s *RetryLayerUserStore) GetByEmail(email string) (*model.User, error) { } +func (s *RetryLayerUserStore) GetByRemoteID(remoteID string) (*model.User, error) { + + tries := 0 + for { + result, err := s.UserStore.GetByRemoteID(remoteID) + if err == nil { + return result, nil + } + if !isRepeatableError(err) { + return result, err + } + tries++ + if tries >= 3 { + err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures") + return result, err + } + timepkg.Sleep(100 * timepkg.Millisecond) + } + +} + func (s *RetryLayerUserStore) GetByUsername(username string) (*model.User, error) { tries := 0 @@ -14621,6 +14843,7 @@ func New(childStore store.Store) *RetryLayer { newStore.CommandStore = &RetryLayerCommandStore{CommandStore: childStore.Command(), Root: &newStore} newStore.CommandWebhookStore = &RetryLayerCommandWebhookStore{CommandWebhookStore: childStore.CommandWebhook(), Root: &newStore} newStore.ComplianceStore = &RetryLayerComplianceStore{ComplianceStore: childStore.Compliance(), Root: &newStore} + newStore.DesktopTokensStore = &RetryLayerDesktopTokensStore{DesktopTokensStore: childStore.DesktopTokens(), Root: &newStore} newStore.DraftStore = &RetryLayerDraftStore{DraftStore: childStore.Draft(), Root: &newStore} newStore.EmojiStore = &RetryLayerEmojiStore{EmojiStore: childStore.Emoji(), Root: &newStore} newStore.FileInfoStore = &RetryLayerFileInfoStore{FileInfoStore: childStore.FileInfo(), Root: &newStore} diff --git a/server/channels/store/retrylayer/retrylayer_test.go b/server/channels/store/retrylayer/retrylayer_test.go index 5489f2fd036..64919f439e1 100644 --- a/server/channels/store/retrylayer/retrylayer_test.go +++ b/server/channels/store/retrylayer/retrylayer_test.go @@ -59,6 +59,7 @@ func genStore() *mocks.Store { mock.On("PostAcknowledgement").Return(&mocks.PostAcknowledgementStore{}) mock.On("PostPersistentNotification").Return(&mocks.PostPersistentNotificationStore{}) mock.On("TrueUpReview").Return(&mocks.TrueUpReviewStore{}) + mock.On("DesktopTokens").Return(&mocks.DesktopTokensStore{}) return mock } diff --git a/server/channels/store/searchtest/testlib.go b/server/channels/store/searchtest/testlib.go index 3a6c257bbac..3777a9544a0 100644 --- a/server/channels/store/searchtest/testlib.go +++ b/server/channels/store/searchtest/testlib.go @@ -6,8 +6,9 @@ package searchtest import ( "testing" + pUtils "github.com/mattermost/mattermost/server/public/utils" + "github.com/mattermost/mattermost/server/v8/channels/store" - "github.com/mattermost/mattermost/server/v8/channels/utils" ) const ( @@ -35,12 +36,12 @@ type searchTest struct { func filterTestsByTag(tests []searchTest, tags ...string) []searchTest { filteredTests := []searchTest{} for _, test := range tests { - if utils.StringInSlice(EngineAll, test.Tags) { + if pUtils.Contains(test.Tags, EngineAll) { filteredTests = append(filteredTests, test) continue } for _, tag := range tags { - if utils.StringInSlice(tag, test.Tags) { + if pUtils.Contains(test.Tags, tag) { filteredTests = append(filteredTests, test) break } diff --git a/server/channels/store/sqlstore/channel_member_history_store.go b/server/channels/store/sqlstore/channel_member_history_store.go index ee98cb0b0ff..6bcbbd652ec 100644 --- a/server/channels/store/sqlstore/channel_member_history_store.go +++ b/server/channels/store/sqlstore/channel_member_history_store.go @@ -175,6 +175,7 @@ func (s SqlChannelMemberHistoryStore) PermanentDeleteBatchForRetentionPolicies(n NowMillis: now, GlobalPolicyEndTime: globalPolicyEndTime, Limit: limit, + StoreDeletedIds: false, }, s.SqlStore, cursor) } diff --git a/server/channels/store/sqlstore/channel_store.go b/server/channels/store/sqlstore/channel_store.go index 00c9bf31adf..81d3f51795f 100644 --- a/server/channels/store/sqlstore/channel_store.go +++ b/server/channels/store/sqlstore/channel_store.go @@ -1468,7 +1468,15 @@ func (s SqlChannelStore) GetByName(teamId string, name string, allowFromCache bo return s.getByName(teamId, name, false, allowFromCache) } +func (s SqlChannelStore) GetByNamesIncludeDeleted(teamId string, names []string, allowFromCache bool) ([]*model.Channel, error) { + return s.getByNames(teamId, names, allowFromCache, true) +} + func (s SqlChannelStore) GetByNames(teamId string, names []string, allowFromCache bool) ([]*model.Channel, error) { + return s.getByNames(teamId, names, allowFromCache, false) +} + +func (s SqlChannelStore) getByNames(teamId string, names []string, allowFromCache, includeArchivedChannels bool) ([]*model.Channel, error) { var channels []*model.Channel if allowFromCache { @@ -1481,7 +1489,9 @@ func (s SqlChannelStore) GetByNames(teamId string, names []string, allowFromCach visited[name] = struct{}{} var cacheItem *model.Channel if err := channelByNameCache.Get(teamId+name, &cacheItem); err == nil { - channels = append(channels, cacheItem) + if includeArchivedChannels || cacheItem.DeleteAt == 0 { + channels = append(channels, cacheItem) + } } else { misses = append(misses, name) } @@ -1490,15 +1500,17 @@ func (s SqlChannelStore) GetByNames(teamId string, names []string, allowFromCach } if len(names) > 0 { + cond := sq.And{ + sq.Eq{"Name": names}, + } + if !includeArchivedChannels { + cond = append(cond, sq.Eq{"DeleteAt": 0}) + } + builder := s.getQueryBuilder(). Select("*"). From("Channels"). - Where( - sq.And{ - sq.Eq{"Name": names}, - sq.Eq{"DeleteAt": 0}, - }, - ) + Where(cond) if teamId != "" { builder = builder.Where(sq.Eq{"TeamId": teamId}) @@ -2020,6 +2032,82 @@ func (s SqlChannelStore) GetChannelMembersTimezones(channelId string) ([]model.S return dbMembersTimezone, nil } +func (s SqlChannelStore) GetChannelsWithUnreadsAndWithMentions(ctx context.Context, channelIDs []string, userID string, userNotifyProps model.StringMap) ([]string, []string, map[string]int64, error) { + query := s.getQueryBuilder().Select( + "Channels.Id", + "Channels.Type", + "Channels.TotalMsgCount", + "Channels.LastPostAt", + "ChannelMembers.MsgCount", + "ChannelMembers.MentionCount", + "ChannelMembers.NotifyProps", + "ChannelMembers.LastViewedAt", + ). + From("ChannelMembers"). + InnerJoin("Channels ON ChannelMembers.ChannelId = Channels.Id"). + Where(sq.Eq{ + "ChannelMembers.ChannelId": channelIDs, + "ChannelMembers.UserId": userID, + }) + + queryString, args, err := query.ToSql() + if err != nil { + return nil, nil, nil, errors.Wrap(err, "channel_tosql") + } + + var channels []struct { + Id string + Type string + TotalMsgCount int + LastPostAt int64 + MsgCount int + MentionCount int + NotifyProps model.StringMap + LastViewedAt int64 + } + + err = s.GetReplicaX().Select(&channels, queryString, args...) + if err != nil { + return nil, nil, nil, errors.Wrap(err, "failed to find channels with unreads and with mentions data") + } + + channelsWithUnreads := []string{} + channelsWithMentions := []string{} + readTimes := map[string]int64{} + + for i := range channels { + channel := channels[i] + hasMentions := (channel.MentionCount > 0) + hasUnreads := (channel.TotalMsgCount-channel.MsgCount > 0) || hasMentions + + if hasUnreads { + channelsWithUnreads = append(channelsWithUnreads, channel.Id) + } + + notify := channel.NotifyProps[model.PushNotifyProp] + if notify == model.ChannelNotifyDefault { + notify = userNotifyProps[model.PushNotifyProp] + } + if notify == model.UserNotifyAll || channel.Type == string(model.ChannelTypeDirect) { + if hasUnreads { + channelsWithMentions = append(channelsWithMentions, channel.Id) + } + } else if notify == model.UserNotifyMention { + if hasMentions { + channelsWithMentions = append(channelsWithMentions, channel.Id) + } + } + + if channel.LastPostAt > channel.LastViewedAt { + readTimes[channel.Id] = channel.LastPostAt + } else { + readTimes[channel.Id] = channel.LastViewedAt + } + } + + return channelsWithUnreads, channelsWithMentions, readTimes, nil +} + func (s SqlChannelStore) GetMember(ctx context.Context, channelID string, userID string) (*model.ChannelMember, error) { selectSQL, args, err := s.channelMembersForTeamWithSchemeSelectQuery. Where(sq.Eq{ @@ -2224,6 +2312,10 @@ func (s SqlChannelStore) GetChannelsMemberCount(channelIDs []string) (_ map[stri defer rows.Close() memberCounts := make(map[string]int64) + // Initialize member counts for channels with zero members + for _, channelID := range channelIDs { + memberCounts[channelID] = 0 + } for rows.Next() { var channelID string var count int64 @@ -2512,6 +2604,10 @@ func (s SqlChannelStore) UpdateLastViewedAt(channelIds []string, userId string) TotalMsgCountRoot int64 }{} + if len(channelIds) == 0 { + return map[string]int64{}, nil + } + // We use the question placeholder format for both databases, because // we replace that with the dollar format later on. // It's needed to support the prefix CTE query. See: https://github.com/Masterminds/squirrel/issues/285. @@ -3426,9 +3522,14 @@ func (s SqlChannelStore) channelSearchQuery(opts *store.ChannelSearchOpts) sq.Se InnerJoin("RetentionPoliciesChannels ON c.Id = RetentionPoliciesChannels.ChannelId"). Where(sq.Eq{"RetentionPoliciesChannels.PolicyId": opts.PolicyID}) } else if opts.ExcludePolicyConstrained { - query = query. - LeftJoin("RetentionPoliciesChannels ON c.Id = RetentionPoliciesChannels.ChannelId"). - Where("RetentionPoliciesChannels.ChannelId IS NULL") + if s.DriverName() == model.DatabaseDriverPostgres { + query = query. + LeftJoin("RetentionPoliciesChannels ON c.Id = RetentionPoliciesChannels.ChannelId"). + Where("RetentionPoliciesChannels.ChannelId IS NULL") + + } else { + query = query.Where(sq.Expr(`c.Id NOT IN (SELECT ChannelId FROM RetentionPoliciesChannels)`)) + } } else if opts.IncludePolicyID { query = query. LeftJoin("RetentionPoliciesChannels ON c.Id = RetentionPoliciesChannels.ChannelId") @@ -4111,34 +4212,36 @@ func (s SqlChannelStore) GetAllChannelsForExportAfter(limit int, afterId string) return channels, nil } -func (s SqlChannelStore) GetChannelMembersForExport(userId string, teamId string) ([]*model.ChannelMemberForExport, error) { +func (s SqlChannelStore) GetChannelMembersForExport(userId string, teamId string, includeArchivedChannel bool) ([]*model.ChannelMemberForExport, error) { members := []*model.ChannelMemberForExport{} - err := s.GetReplicaX().Select(&members, ` - SELECT - ChannelMembers.ChannelId, - ChannelMembers.UserId, - ChannelMembers.Roles, - ChannelMembers.LastViewedAt, - ChannelMembers.MsgCount, - ChannelMembers.MentionCount, - ChannelMembers.MentionCountRoot, - COALESCE(ChannelMembers.UrgentMentionCount, 0) AS UrgentMentionCount, - ChannelMembers.MsgCountRoot, - ChannelMembers.NotifyProps, - ChannelMembers.LastUpdateAt, - ChannelMembers.SchemeUser, - ChannelMembers.SchemeAdmin, - (ChannelMembers.SchemeGuest IS NOT NULL AND ChannelMembers.SchemeGuest) as SchemeGuest, - Channels.Name as ChannelName - FROM - ChannelMembers - INNER JOIN - Channels ON ChannelMembers.ChannelId = Channels.Id - WHERE - ChannelMembers.UserId = ? - AND Channels.TeamId = ? - AND Channels.DeleteAt = 0`, - userId, teamId) + q := ` + SELECT + ChannelMembers.ChannelId, + ChannelMembers.UserId, + ChannelMembers.Roles, + ChannelMembers.LastViewedAt, + ChannelMembers.MsgCount, + ChannelMembers.MentionCount, + ChannelMembers.MentionCountRoot, + COALESCE(ChannelMembers.UrgentMentionCount, 0) AS UrgentMentionCount, + ChannelMembers.MsgCountRoot, + ChannelMembers.NotifyProps, + ChannelMembers.LastUpdateAt, + ChannelMembers.SchemeUser, + ChannelMembers.SchemeAdmin, + (ChannelMembers.SchemeGuest IS NOT NULL AND ChannelMembers.SchemeGuest) as SchemeGuest, + Channels.Name as ChannelName + FROM + ChannelMembers + INNER JOIN + Channels ON ChannelMembers.ChannelId = Channels.Id + WHERE + ChannelMembers.UserId = ? + AND Channels.TeamId = ?` + if !includeArchivedChannel { + q += " AND Channels.DeleteAt = 0" + } + err := s.GetReplicaX().Select(&members, q, userId, teamId) if err != nil { return nil, errors.Wrap(err, "failed to find Channels for export") } diff --git a/server/channels/store/sqlstore/channel_store_categories.go b/server/channels/store/sqlstore/channel_store_categories.go index ca20e281e3e..7b4e55d4d74 100644 --- a/server/channels/store/sqlstore/channel_store_categories.go +++ b/server/channels/store/sqlstore/channel_store_categories.go @@ -553,6 +553,10 @@ func (s SqlChannelStore) getSidebarCategoriesT(db dbSelecter, userId string, opt query = query.Where(sq.Eq{"SidebarCategories.TeamId": opts.TeamID}) } + if opts.Type != "" { + query = query.Where(sq.Eq{"SidebarCategories.Type": opts.Type}) + } + sql, args, err := query.ToSql() if err != nil { return nil, errors.Wrap(err, "sidebar_categories_tosql") @@ -1125,3 +1129,18 @@ func (s SqlChannelStore) DeleteSidebarCategory(categoryId string) (err error) { return nil } + +func (s SqlChannelStore) DeleteAllSidebarChannelForChannel(channelID string) error { + query, args, err := s.getQueryBuilder(). + Delete("SidebarChannels"). + Where(sq.Eq{ + "ChannelId": channelID, + }).ToSql() + + if err != nil { + return errors.Wrap(err, "delete_all_sidebar_channel_for_channel_to_sql") + } + + _, err = s.GetMasterX().Exec(query, args...) + return err +} diff --git a/server/channels/store/sqlstore/context.go b/server/channels/store/sqlstore/context.go index 859fbe00278..52f9cc2df3f 100644 --- a/server/channels/store/sqlstore/context.go +++ b/server/channels/store/sqlstore/context.go @@ -5,6 +5,8 @@ package sqlstore import ( "context" + + "github.com/mattermost/mattermost/server/public/shared/request" ) // storeContextKey is the base type for all context keys for the store. @@ -19,10 +21,20 @@ const ( ) // WithMaster adds the context value that master DB should be selected for this request. +// +// Deprecated: This method is deprecated and there's ongoing change to use `request.CTX` across +// instead of `context.Context`. Please use `RequestContextWithMaster` instead. func WithMaster(ctx context.Context) context.Context { return context.WithValue(ctx, storeContextKey(useMaster), true) } +// RequestContextWithMaster adds the context value that master DB should be selected for this request. +func RequestContextWithMaster(c request.CTX) request.CTX { + ctx := WithMaster(c.Context()) + c.SetContext(ctx) + return c +} + // hasMaster is a helper function to check whether master DB should be selected or not. func hasMaster(ctx context.Context) bool { if v := ctx.Value(storeContextKey(useMaster)); v != nil { diff --git a/server/channels/store/sqlstore/desktop_tokens_store.go b/server/channels/store/sqlstore/desktop_tokens_store.go new file mode 100644 index 00000000000..eefce1d0241 --- /dev/null +++ b/server/channels/store/sqlstore/desktop_tokens_store.go @@ -0,0 +1,123 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package sqlstore + +import ( + "database/sql" + + "github.com/mattermost/mattermost/server/v8/channels/store" + "github.com/mattermost/mattermost/server/v8/einterfaces" + sq "github.com/mattermost/squirrel" + "github.com/pkg/errors" +) + +type SqlDesktopTokensStore struct { + *SqlStore + metrics einterfaces.MetricsInterface +} + +func newSqlDesktopTokensStore(sqlStore *SqlStore, metrics einterfaces.MetricsInterface) store.DesktopTokensStore { + return &SqlDesktopTokensStore{ + SqlStore: sqlStore, + metrics: metrics, + } +} + +func (s *SqlDesktopTokensStore) GetUserId(token string, minCreateAt int64) (*string, error) { + query := s.getQueryBuilder(). + Select("UserId"). + From("DesktopTokens"). + Where(sq.And{ + sq.Eq{"Token": token}, + sq.GtOrEq{"CreateAt": minCreateAt}, + }) + + dt := struct{ UserId string }{} + err := s.GetReplicaX().GetBuilder(&dt, query) + + if err != nil { + if err == sql.ErrNoRows { + return nil, store.NewErrNotFound("DesktopTokens", token) + } + return nil, errors.Wrapf(err, "No token for %s", token) + } + + return &dt.UserId, nil +} + +func (s *SqlDesktopTokensStore) Insert(token string, createAt int64, userId string) error { + builder := s.getQueryBuilder(). + Insert("DesktopTokens"). + Columns("Token", "CreateAt", "UserId"). + Values(token, createAt, userId) + + query, args, err := builder.ToSql() + + if err != nil { + return errors.Wrap(err, "insert_desktoptokens_tosql") + } + + if _, err = s.GetMasterX().Exec(query, args...); err != nil { + return errors.Wrap(err, "failed to insert token row") + } + + return nil +} + +func (s *SqlDesktopTokensStore) Delete(token string) error { + builder := s.getQueryBuilder(). + Delete("DesktopTokens"). + Where(sq.Eq{ + "Token": token, + }) + + query, args, err := builder.ToSql() + + if err != nil { + return errors.Wrap(err, "delete_desktoptokens_tosql") + } + + if _, err = s.GetMasterX().Exec(query, args...); err != nil { + return errors.Wrap(err, "failed to delete token row") + } + return nil +} + +func (s *SqlDesktopTokensStore) DeleteByUserId(userId string) error { + builder := s.getQueryBuilder(). + Delete("DesktopTokens"). + Where(sq.Eq{ + "UserId": userId, + }) + + query, args, err := builder.ToSql() + + if err != nil { + return errors.Wrap(err, "delete_by_userid_desktoptokens_tosql") + } + + if _, err = s.GetMasterX().Exec(query, args...); err != nil { + return errors.Wrap(err, "failed to delete token row") + } + return nil +} + +func (s *SqlDesktopTokensStore) DeleteOlderThan(minCreateAt int64) error { + builder := s.getQueryBuilder(). + Delete("DesktopTokens"). + Where(sq.Lt{ + "CreateAt": minCreateAt, + }) + + query, args, err := builder.ToSql() + + if err != nil { + return errors.Wrap(err, "delete_old_desktoptokens_tosql") + } + + if _, err = s.GetMasterX().Exec(query, args...); err != nil { + return errors.Wrap(err, "failed to delete token row") + } + return nil +} diff --git a/server/channels/store/sqlstore/desktop_tokens_store_test.go b/server/channels/store/sqlstore/desktop_tokens_store_test.go new file mode 100644 index 00000000000..7f4bc4583ef --- /dev/null +++ b/server/channels/store/sqlstore/desktop_tokens_store_test.go @@ -0,0 +1,14 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package sqlstore + +import ( + "testing" + + "github.com/mattermost/mattermost/server/v8/channels/store/storetest" +) + +func TestDesktopTokensStore(t *testing.T) { + StoreTestWithSqlStore(t, storetest.TestDesktopTokensStore) +} diff --git a/server/channels/store/sqlstore/emoji_store.go b/server/channels/store/sqlstore/emoji_store.go index 74299668678..df394813f53 100644 --- a/server/channels/store/sqlstore/emoji_store.go +++ b/server/channels/store/sqlstore/emoji_store.go @@ -4,7 +4,6 @@ package sqlstore import ( - "context" "database/sql" "fmt" "strings" @@ -12,6 +11,7 @@ import ( "github.com/pkg/errors" "github.com/mattermost/mattermost/server/public/model" + "github.com/mattermost/mattermost/server/public/shared/request" "github.com/mattermost/mattermost/server/v8/channels/store" "github.com/mattermost/mattermost/server/v8/einterfaces" ) @@ -44,21 +44,21 @@ func (es SqlEmojiStore) Save(emoji *model.Emoji) (*model.Emoji, error) { return emoji, nil } -func (es SqlEmojiStore) Get(ctx context.Context, id string, allowFromCache bool) (*model.Emoji, error) { - return es.getBy(ctx, "Id", id) +func (es SqlEmojiStore) Get(c request.CTX, id string, allowFromCache bool) (*model.Emoji, error) { + return es.getBy(c, "Id", id) } -func (es SqlEmojiStore) GetByName(ctx context.Context, name string, allowFromCache bool) (*model.Emoji, error) { - return es.getBy(ctx, "Name", name) +func (es SqlEmojiStore) GetByName(c request.CTX, name string, allowFromCache bool) (*model.Emoji, error) { + return es.getBy(c, "Name", name) } -func (es SqlEmojiStore) GetMultipleByName(ctx context.Context, names []string) ([]*model.Emoji, error) { +func (es SqlEmojiStore) GetMultipleByName(c request.CTX, names []string) ([]*model.Emoji, error) { // Creating (?, ?, ?) len(names) number of times. keys := strings.Join(strings.Fields(strings.Repeat("? ", len(names))), ",") args := makeStringArgs(names) emojis := []*model.Emoji{} - if err := es.DBXFromContext(ctx).Select(&emojis, + if err := es.DBXFromContext(c.Context()).Select(&emojis, `SELECT * FROM @@ -134,10 +134,10 @@ func (es SqlEmojiStore) Search(name string, prefixOnly bool, limit int) ([]*mode } // getBy returns one active (not deleted) emoji, found by any one column (what/key). -func (es SqlEmojiStore) getBy(ctx context.Context, what, key string) (*model.Emoji, error) { +func (es SqlEmojiStore) getBy(c request.CTX, what, key string) (*model.Emoji, error) { var emoji model.Emoji - err := es.DBXFromContext(ctx).Get(&emoji, + err := es.DBXFromContext(c.Context()).Get(&emoji, `SELECT * FROM diff --git a/server/channels/store/sqlstore/group_store.go b/server/channels/store/sqlstore/group_store.go index 0d5d0cbbec5..4286bd6eb42 100644 --- a/server/channels/store/sqlstore/group_store.go +++ b/server/channels/store/sqlstore/group_store.go @@ -215,7 +215,12 @@ func (s *SqlGroupStore) buildInsertGroupUsersQuery(groupId string, userIds []str func (s *SqlGroupStore) Get(groupId string) (*model.Group, error) { var group model.Group - if err := s.GetReplicaX().Get(&group, "SELECT * from UserGroups WHERE Id = ?", groupId); err != nil { + builder := s.getQueryBuilder(). + Select("*"). + From("UserGroups"). + Where(sq.Eq{"Id": groupId}) + + if err := s.GetReplicaX().GetBuilder(&group, builder); err != nil { if err == sql.ErrNoRows { return nil, store.NewErrNotFound("Group", groupId) } @@ -261,7 +266,15 @@ func (s *SqlGroupStore) GetByIDs(groupIDs []string) ([]*model.Group, error) { func (s *SqlGroupStore) GetByRemoteID(remoteID string, groupSource model.GroupSource) (*model.Group, error) { var group model.Group - if err := s.GetReplicaX().Get(&group, "SELECT * from UserGroups WHERE RemoteId = ? AND Source = ?", remoteID, groupSource); err != nil { + builder := s.getQueryBuilder(). + Select("*"). + From("UserGroups"). + Where(sq.Eq{ + "RemoteId": remoteID, + "Source": groupSource, + }) + + if err := s.GetReplicaX().GetBuilder(&group, builder); err != nil { if err == sql.ErrNoRows { return nil, store.NewErrNotFound("Group", fmt.Sprintf("remoteId=%s", remoteID)) } @@ -273,8 +286,15 @@ func (s *SqlGroupStore) GetByRemoteID(remoteID string, groupSource model.GroupSo func (s *SqlGroupStore) GetAllBySource(groupSource model.GroupSource) ([]*model.Group, error) { groups := []*model.Group{} + builder := s.getQueryBuilder(). + Select("*"). + From("UserGroups"). + Where(sq.Eq{ + "DeleteAt": 0, + "Source": groupSource, + }) - if err := s.GetReplicaX().Select(&groups, "SELECT * from UserGroups WHERE DeleteAt = 0 AND Source = ?", groupSource); err != nil { + if err := s.GetReplicaX().SelectBuilder(&groups, builder); err != nil { return nil, errors.Wrapf(err, "failed to find Groups by groupSource=%v", groupSource) } @@ -284,17 +304,16 @@ func (s *SqlGroupStore) GetAllBySource(groupSource model.GroupSource) ([]*model. func (s *SqlGroupStore) GetByUser(userId string) ([]*model.Group, error) { groups := []*model.Group{} - query := ` - SELECT - UserGroups.* - FROM - GroupMembers - JOIN UserGroups ON UserGroups.Id = GroupMembers.GroupId - WHERE - GroupMembers.DeleteAt = 0 - AND UserId = ?` + builder := s.getQueryBuilder(). + Select("UserGroups.*"). + From("GroupMembers"). + Join("UserGroups ON UserGroups.Id = GroupMembers.GroupId"). + Where(sq.Eq{ + "GroupMembers.DeleteAt": 0, + "UserId": userId, + }) - if err := s.GetReplicaX().Select(&groups, query, userId); err != nil { + if err := s.GetReplicaX().SelectBuilder(&groups, builder); err != nil { return nil, errors.Wrapf(err, "failed to find Groups with userId=%s", userId) } @@ -303,7 +322,12 @@ func (s *SqlGroupStore) GetByUser(userId string) ([]*model.Group, error) { func (s *SqlGroupStore) Update(group *model.Group) (*model.Group, error) { var retrievedGroup model.Group - if err := s.GetReplicaX().Get(&retrievedGroup, "SELECT * FROM UserGroups WHERE Id = ?", group.Id); err != nil { + builder := s.getQueryBuilder(). + Select("*"). + From("UserGroups"). + Where(sq.Eq{"Id": group.Id}) + + if err := s.GetReplicaX().GetBuilder(&retrievedGroup, builder); err != nil { if err == sql.ErrNoRows { return nil, store.NewErrNotFound("Group", group.Id) } @@ -343,7 +367,15 @@ func (s *SqlGroupStore) Update(group *model.Group) (*model.Group, error) { func (s *SqlGroupStore) Delete(groupID string) (*model.Group, error) { var group model.Group - if err := s.GetReplicaX().Get(&group, "SELECT * from UserGroups WHERE Id = ? AND DeleteAt = 0", groupID); err != nil { + builder := s.getQueryBuilder(). + Select("*"). + From("UserGroups"). + Where(sq.Eq{ + "Id": groupID, + "DeleteAt": 0, + }) + + if err := s.GetReplicaX().GetBuilder(&group, builder); err != nil { if err == sql.ErrNoRows { return nil, store.NewErrNotFound("Group", groupID) } @@ -351,9 +383,11 @@ func (s *SqlGroupStore) Delete(groupID string) (*model.Group, error) { } time := model.GetMillis() + group.DeleteAt = time + group.UpdateAt = time if _, err := s.GetMasterX().Exec(`UPDATE UserGroups SET DeleteAt=?, UpdateAt=? - WHERE Id=? AND DeleteAt=0`, time, time, groupID); err != nil { + WHERE Id=? AND DeleteAt=0`, group.DeleteAt, group.UpdateAt, groupID); err != nil { return nil, errors.Wrapf(err, "failed to update Group with id=%s", groupID) } @@ -362,17 +396,26 @@ func (s *SqlGroupStore) Delete(groupID string) (*model.Group, error) { func (s *SqlGroupStore) Restore(groupID string) (*model.Group, error) { var group model.Group - if err := s.GetReplicaX().Get(&group, "SELECT * from UserGroups WHERE Id = ? AND DeleteAt != 0", groupID); err != nil { + builder := s.getQueryBuilder(). + Select("*"). + From("UserGroups"). + Where(sq.And{ + sq.Eq{"Id": groupID}, + sq.NotEq{"DeleteAt": 0}, + }) + + if err := s.GetReplicaX().GetBuilder(&group, builder); err != nil { if err == sql.ErrNoRows { return nil, store.NewErrNotFound("Group", groupID) } return nil, errors.Wrapf(err, "failed to get Group with id=%s", groupID) } - time := model.GetMillis() + group.UpdateAt = model.GetMillis() + group.DeleteAt = 0 if _, err := s.GetMasterX().Exec(`UPDATE UserGroups SET DeleteAt=0, UpdateAt=? - WHERE Id=? AND DeleteAt!=0`, time, groupID); err != nil { + WHERE Id=? AND DeleteAt!=0`, group.UpdateAt, groupID); err != nil { return nil, errors.Wrapf(err, "failed to update Group with id=%s", groupID) } @@ -380,40 +423,33 @@ func (s *SqlGroupStore) Restore(groupID string) (*model.Group, error) { } func (s *SqlGroupStore) GetMember(groupID, userID string) (*model.GroupMember, error) { - query, args, err := s.getQueryBuilder(). + builder := s.getQueryBuilder(). Select("*"). From("GroupMembers"). Where(sq.Eq{"UserId": userID}). Where(sq.Eq{"GroupId": groupID}). - Where(sq.Eq{"DeleteAt": 0}). - ToSql() - if err != nil { - return nil, errors.Wrap(err, "get_member_query") - } + Where(sq.Eq{"DeleteAt": 0}) var groupMember model.GroupMember - err = s.GetReplicaX().Get(&groupMember, query, args...) - if err != nil { + if err := s.GetReplicaX().GetBuilder(&groupMember, builder); err != nil { return nil, errors.Wrap(err, "GetMember") } - return &groupMember, nil } func (s *SqlGroupStore) GetMemberUsers(groupID string) ([]*model.User, error) { groupMembers := []*model.User{} - query := ` - SELECT - Users.* - FROM - GroupMembers - JOIN Users ON Users.Id = GroupMembers.UserId - WHERE - GroupMembers.DeleteAt = 0 - AND Users.DeleteAt = 0 - AND GroupId = ?` + builder := s.getQueryBuilder(). + Select("Users.*"). + From("GroupMembers"). + Join("Users ON Users.Id = GroupMembers.UserId"). + Where(sq.Eq{ + "GroupMembers.DeleteAt": 0, + "Users.DeleteAt": 0, + "GroupId": groupID, + }) - if err := s.GetReplicaX().Select(&groupMembers, query, groupID); err != nil { + if err := s.GetReplicaX().SelectBuilder(&groupMembers, builder); err != nil { return nil, errors.Wrapf(err, "failed to find member Users for Group with id=%s", groupID) } @@ -485,11 +521,16 @@ func (s *SqlGroupStore) GetMemberUsersSortedPage(groupID string, page int, perPa func (s *SqlGroupStore) GetNonMemberUsersPage(groupID string, page int, perPage int, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, error) { groupMembers := []*model.User{} - if err := s.GetReplicaX().Get(&model.Group{}, "SELECT * FROM UserGroups WHERE Id = ?", groupID); err != nil { + builder := s.getQueryBuilder(). + Select("*"). + From("UserGroups"). + Where(sq.Eq{"Id": groupID}) + + if err := s.GetReplicaX().GetBuilder(&model.Group{}, builder); err != nil { return nil, errors.Wrap(err, "GetNonMemberUsersPage") } - query := s.getQueryBuilder(). + builder = s.getQueryBuilder(). Select("u.*"). From("Users u"). LeftJoin("GroupMembers ON (GroupMembers.UserId = u.Id AND GroupMembers.GroupId = ?)", groupID). @@ -499,14 +540,9 @@ func (s *SqlGroupStore) GetNonMemberUsersPage(groupID string, page int, perPage Offset(uint64(page * perPage)). OrderBy("u.Username ASC") - query = applyViewRestrictionsFilter(query, viewRestrictions, true) + builder = applyViewRestrictionsFilter(builder, viewRestrictions, true) - queryString, args, err := query.ToSql() - if err != nil { - return nil, errors.Wrap(err, "") - } - - if err := s.GetReplicaX().Select(&groupMembers, queryString, args...); err != nil { + if err := s.GetReplicaX().SelectBuilder(&groupMembers, builder); err != nil { return nil, errors.Wrapf(err, "failed to find member Users for Group with id=%s", groupID) } @@ -630,7 +666,10 @@ func (s *SqlGroupStore) DeleteMember(groupID string, userID string) (*model.Grou } func (s *SqlGroupStore) PermanentDeleteMembersByUser(userId string) error { - if _, err := s.GetMasterX().Exec("DELETE FROM GroupMembers WHERE UserId = ?", userId); err != nil { + builder := s.getQueryBuilder(). + Delete("GroupMembers"). + Where(sq.Eq{"UserId": userId}) + if _, err := s.GetMasterX().ExecBuilder(builder); err != nil { return errors.Wrapf(err, "failed to permanent delete GroupMember with userId=%s", userId) } return nil @@ -941,15 +980,9 @@ func (s *SqlGroupStore) TeamMembersToAdd(since int64, teamID *string, includeRem builder = builder.Where(sq.Eq{"Teams.Id": *teamID}) } - query, params, err := builder.ToSql() - if err != nil { - return nil, errors.Wrap(err, "team_members_to_add_tosql") - } - teamMembers := []*model.UserTeamIDPair{} - err = s.GetMasterX().Select(&teamMembers, query, params...) - if err != nil { + if err := s.GetMasterX().SelectBuilder(&teamMembers, builder); err != nil { return nil, errors.Wrap(err, "failed to find UserTeamIDPairs") } @@ -986,15 +1019,9 @@ func (s *SqlGroupStore) ChannelMembersToAdd(since int64, channelID *string, incl builder = builder.Where(sq.Eq{"Channels.Id": *channelID}) } - query, params, err := builder.ToSql() - if err != nil { - return nil, errors.Wrap(err, "channel_members_to_add_tosql") - } - channelMembers := []*model.UserChannelIDPair{} - err = s.GetMasterX().Select(&channelMembers, query, params...) - if err != nil { + if err := s.GetMasterX().SelectBuilder(&channelMembers, builder); err != nil { return nil, errors.Wrap(err, "failed to find UserChannelIDPairs") } @@ -1057,15 +1084,9 @@ func (s *SqlGroupStore) TeamMembersToRemove(teamID *string) ([]*model.TeamMember builder = builder.Where(sq.Eq{"TeamMembers.TeamId": *teamID}) } - query, params, err := builder.ToSql() - if err != nil { - return nil, errors.Wrap(err, "team_members_to_remove_tosql") - } - teamMembers := []*model.TeamMember{} - err = s.GetReplicaX().Select(&teamMembers, query, params...) - if err != nil { + if err := s.GetReplicaX().SelectBuilder(&teamMembers, builder); err != nil { return nil, errors.Wrap(err, "failed to find TeamMembers") } @@ -1073,16 +1094,10 @@ func (s *SqlGroupStore) TeamMembersToRemove(teamID *string) ([]*model.TeamMember } func (s *SqlGroupStore) CountGroupsByChannel(channelId string, opts model.GroupSearchOpts) (int64, error) { - countQuery := s.groupsBySyncableBaseQuery(model.GroupSyncableTypeChannel, selectCountGroups, channelId, opts) - - countQueryString, args, err := countQuery.ToSql() - if err != nil { - return int64(0), errors.Wrap(err, "count_groups_by_channel_tosql") - } + builder := s.groupsBySyncableBaseQuery(model.GroupSyncableTypeChannel, selectCountGroups, channelId, opts) var count int64 - err = s.GetReplicaX().Get(&count, countQueryString, args...) - if err != nil { + if err := s.GetReplicaX().GetBuilder(&count, builder); err != nil { return int64(0), errors.Wrapf(err, "failed to count Groups by channel with channelId=%s", channelId) } @@ -1186,21 +1201,15 @@ func (groups groupsAssociatedToChannelWithSchemeAdmin) ToModel() []*model.Groups } func (s *SqlGroupStore) GetGroupsByChannel(channelId string, opts model.GroupSearchOpts) ([]*model.GroupWithSchemeAdmin, error) { - query := s.groupsBySyncableBaseQuery(model.GroupSyncableTypeChannel, selectGroups, channelId, opts) + builder := s.groupsBySyncableBaseQuery(model.GroupSyncableTypeChannel, selectGroups, channelId, opts) if opts.PageOpts != nil { offset := uint64(opts.PageOpts.Page * opts.PageOpts.PerPage) - query = query.OrderBy("ug.DisplayName").Limit(uint64(opts.PageOpts.PerPage)).Offset(offset) - } - - queryString, args, err := query.ToSql() - if err != nil { - return nil, errors.Wrap(err, "get_groups_by_channel_tosql") + builder = builder.OrderBy("ug.DisplayName").Limit(uint64(opts.PageOpts.PerPage)).Offset(offset) } groups := groupsWithSchemeAdmin{} - err = s.GetReplicaX().Select(&groups, queryString, args...) - if err != nil { + if err := s.GetReplicaX().SelectBuilder(&groups, builder); err != nil { return nil, errors.Wrapf(err, "failed to find Groups with channelId=%s", channelId) } @@ -1255,15 +1264,9 @@ func (s *SqlGroupStore) ChannelMembersToRemove(channelID *string) ([]*model.Chan builder = builder.Where(sq.Eq{"ChannelMembers.ChannelId": *channelID}) } - query, params, err := builder.ToSql() - if err != nil { - return nil, errors.Wrap(err, "channel_members_to_remove_tosql") - } - channelMembers := []*model.ChannelMember{} - err = s.GetReplicaX().Select(&channelMembers, query, params...) - if err != nil { + if err := s.GetReplicaX().SelectBuilder(&channelMembers, builder); err != nil { return nil, errors.Wrap(err, "failed to find ChannelMembers") } @@ -1384,16 +1387,10 @@ func (s *SqlGroupStore) getGroupsAssociatedToChannelsByTeam(teamID string, opts } func (s *SqlGroupStore) CountGroupsByTeam(teamId string, opts model.GroupSearchOpts) (int64, error) { - countQuery := s.groupsBySyncableBaseQuery(model.GroupSyncableTypeTeam, selectCountGroups, teamId, opts) - - countQueryString, args, err := countQuery.ToSql() - if err != nil { - return int64(0), errors.Wrap(err, "count_groups_by_team_tosql") - } + builder := s.groupsBySyncableBaseQuery(model.GroupSyncableTypeTeam, selectCountGroups, teamId, opts) var count int64 - err = s.GetReplicaX().Get(&count, countQueryString, args...) - if err != nil { + if err := s.GetReplicaX().GetBuilder(&count, builder); err != nil { return int64(0), errors.Wrapf(err, "failed to count Groups with teamId=%s", teamId) } @@ -1401,21 +1398,15 @@ func (s *SqlGroupStore) CountGroupsByTeam(teamId string, opts model.GroupSearchO } func (s *SqlGroupStore) GetGroupsByTeam(teamId string, opts model.GroupSearchOpts) ([]*model.GroupWithSchemeAdmin, error) { - query := s.groupsBySyncableBaseQuery(model.GroupSyncableTypeTeam, selectGroups, teamId, opts) + builder := s.groupsBySyncableBaseQuery(model.GroupSyncableTypeTeam, selectGroups, teamId, opts) if opts.PageOpts != nil { offset := uint64(opts.PageOpts.Page * opts.PageOpts.PerPage) - query = query.OrderBy("ug.DisplayName").Limit(uint64(opts.PageOpts.PerPage)).Offset(offset) - } - - queryString, args, err := query.ToSql() - if err != nil { - return nil, errors.Wrap(err, "get_groups_by_team_tosql") + builder = builder.OrderBy("ug.DisplayName").Limit(uint64(opts.PageOpts.PerPage)).Offset(offset) } groups := groupsWithSchemeAdmin{} - err = s.GetReplicaX().Select(&groups, queryString, args...) - if err != nil { + if err := s.GetReplicaX().SelectBuilder(&groups, builder); err != nil { return nil, errors.Wrapf(err, "failed to find Groups with teamId=%s", teamId) } @@ -1423,22 +1414,16 @@ func (s *SqlGroupStore) GetGroupsByTeam(teamId string, opts model.GroupSearchOpt } func (s *SqlGroupStore) GetGroupsAssociatedToChannelsByTeam(teamId string, opts model.GroupSearchOpts) (map[string][]*model.GroupWithSchemeAdmin, error) { - query := s.getGroupsAssociatedToChannelsByTeam(teamId, opts) + builder := s.getGroupsAssociatedToChannelsByTeam(teamId, opts) if opts.PageOpts != nil { offset := uint64(opts.PageOpts.Page * opts.PageOpts.PerPage) - query = query.OrderBy("ug.DisplayName").Limit(uint64(opts.PageOpts.PerPage)).Offset(offset) - } - - queryString, args, err := query.ToSql() - if err != nil { - return nil, errors.Wrap(err, "get_groups_associated_to_channel_by_team_tosql") + builder = builder.OrderBy("ug.DisplayName").Limit(uint64(opts.PageOpts.PerPage)).Offset(offset) } tgroups := groupsAssociatedToChannelWithSchemeAdmin{} - err = s.GetReplicaX().Select(&tgroups, queryString, args...) - if err != nil { + if err := s.GetReplicaX().SelectBuilder(&tgroups, builder); err != nil { return nil, errors.Wrapf(err, "failed to find Groups with teamId=%s", teamId) } @@ -1529,17 +1514,27 @@ func (s *SqlGroupStore) GetGroups(page, perPage int, opts model.GroupSearchOpts, } groupsQuery = groupsQuery. - From("UserGroups g"). - OrderBy("g.DisplayName") + From("UserGroups g") if opts.Since > 0 { groupsQuery = groupsQuery.Where(sq.Gt{ "g.UpdateAt": opts.Since, }) - } else { + } + + if opts.FilterArchived { + groupsQuery = groupsQuery.Where("g.DeleteAt > 0") + } else if !opts.IncludeArchived && opts.Since <= 0 { + // Mobile needs to return archived groups when the since parameter is set, will need to keep this for backwards compatibility groupsQuery = groupsQuery.Where("g.DeleteAt = 0") } + if opts.IncludeArchived { + groupsQuery = groupsQuery.OrderBy("CASE WHEN g.DeleteAt = 0 THEN g.DisplayName end, CASE WHEN g.DeleteAt != 0 THEN g.DisplayName END") + } else { + groupsQuery = groupsQuery.OrderBy("g.DisplayName") + } + if perPage != 0 { groupsQuery = groupsQuery. Limit(uint64(perPage)). @@ -1685,16 +1680,11 @@ func (s *SqlGroupStore) teamMembersMinusGroupMembersQuery(teamID string, groupID // TeamMembersMinusGroupMembers returns the set of users on the given team minus the set of users in the given // groups. func (s *SqlGroupStore) TeamMembersMinusGroupMembers(teamID string, groupIDs []string, page, perPage int) ([]*model.UserWithGroups, error) { - query := s.teamMembersMinusGroupMembersQuery(teamID, groupIDs, false) - query = query.OrderBy("Users.Username ASC").Limit(uint64(perPage)).Offset(uint64(page * perPage)) - - queryString, args, err := query.ToSql() - if err != nil { - return nil, errors.Wrap(err, "team_members_minus_group_members") - } + builder := s.teamMembersMinusGroupMembersQuery(teamID, groupIDs, false) + builder = builder.OrderBy("Users.Username ASC").Limit(uint64(perPage)).Offset(uint64(page * perPage)) users := []*model.UserWithGroups{} - if err = s.GetReplicaX().Select(&users, queryString, args...); err != nil { + if err := s.GetReplicaX().SelectBuilder(&users, builder); err != nil { return nil, errors.Wrap(err, "failed to find UserWithGroups") } @@ -1762,16 +1752,11 @@ func (s *SqlGroupStore) channelMembersMinusGroupMembersQuery(channelID string, g // ChannelMembersMinusGroupMembers returns the set of users in the given channel minus the set of users in the given // groups. func (s *SqlGroupStore) ChannelMembersMinusGroupMembers(channelID string, groupIDs []string, page, perPage int) ([]*model.UserWithGroups, error) { - query := s.channelMembersMinusGroupMembersQuery(channelID, groupIDs, false) - query = query.OrderBy("Users.Username ASC").Limit(uint64(perPage)).Offset(uint64(page * perPage)) - - queryString, args, err := query.ToSql() - if err != nil { - return nil, errors.Wrap(err, "channel_members_minus_group_members_tosql") - } + builder := s.channelMembersMinusGroupMembersQuery(channelID, groupIDs, false) + builder = builder.OrderBy("Users.Username ASC").Limit(uint64(perPage)).Offset(uint64(page * perPage)) users := []*model.UserWithGroups{} - if err = s.GetReplicaX().Select(&users, queryString, args...); err != nil { + if err := s.GetReplicaX().SelectBuilder(&users, builder); err != nil { return nil, errors.Wrap(err, "failed to find UserWithGroups") } @@ -1824,13 +1809,8 @@ func (s *SqlGroupStore) PermittedSyncableAdmins(syncableID string, syncableType From(fmt.Sprintf("Group%ss", syncableType)). Join(fmt.Sprintf("GroupMembers ON GroupMembers.GroupId = Group%ss.GroupId AND Group%[1]ss.SchemeAdmin = TRUE AND GroupMembers.DeleteAt = 0", syncableType.String())).Where(fmt.Sprintf("Group%[1]ss.%[1]sId = ?", syncableType.String()), syncableID) - query, args, err := builder.ToSql() - if err != nil { - return nil, errors.Wrap(err, "permitted_syncable_admins_tosql") - } - var userIDs []string - if err = s.GetMasterX().Select(&userIDs, query, args...); err != nil { + if err := s.GetMasterX().SelectBuilder(&userIDs, builder); err != nil { return nil, errors.Wrapf(err, "failed to find User ids") } @@ -1868,13 +1848,8 @@ func (s *SqlGroupStore) DistinctGroupMemberCountForSource(source model.GroupSour Join("UserGroups ON GroupMembers.GroupId = UserGroups.Id"). Where(sq.Eq{"UserGroups.Source": source, "GroupMembers.DeleteAt": 0}) - query, args, err := builder.ToSql() - if err != nil { - return 0, errors.Wrap(err, "distinct_group_member_count_for_source_tosql") - } - var count int64 - if err = s.GetReplicaX().Get(&count, query, args...); err != nil { + if err := s.GetReplicaX().GetBuilder(&count, builder); err != nil { return 0, errors.Wrapf(err, "failed to select distinct groupmember count for source %q", source) } diff --git a/server/channels/store/sqlstore/job_store.go b/server/channels/store/sqlstore/job_store.go index 9b56598682b..380910fde8b 100644 --- a/server/channels/store/sqlstore/job_store.go +++ b/server/channels/store/sqlstore/job_store.go @@ -16,6 +16,7 @@ import ( "github.com/pkg/errors" "github.com/mattermost/mattermost/server/public/model" + "github.com/mattermost/mattermost/server/public/shared/request" "github.com/mattermost/mattermost/server/v8/channels/store" ) @@ -201,7 +202,7 @@ func (jss SqlJobStore) UpdateStatusOptimistically(id string, currentStatus strin return true, nil } -func (jss SqlJobStore) Get(id string) (*model.Job, error) { +func (jss SqlJobStore) Get(c *request.Context, id string) (*model.Job, error) { query, args, err := jss.getQueryBuilder(). Select("*"). From("Jobs"). @@ -209,6 +210,7 @@ func (jss SqlJobStore) Get(id string) (*model.Job, error) { if err != nil { return nil, errors.Wrap(err, "job_tosql") } + var status model.Job if err = jss.GetReplicaX().Get(&status, query, args...); err != nil { if err == sql.ErrNoRows { @@ -216,28 +218,13 @@ func (jss SqlJobStore) Get(id string) (*model.Job, error) { } return nil, errors.Wrapf(err, "failed to get Job with id=%s", id) } + + status.InitLogger(c.Logger()) + return &status, nil } -func (jss SqlJobStore) GetAllPage(offset int, limit int) ([]*model.Job, error) { - query, args, err := jss.getQueryBuilder(). - Select("*"). - From("Jobs"). - OrderBy("CreateAt DESC"). - Limit(uint64(limit)). - Offset(uint64(offset)).ToSql() - if err != nil { - return nil, errors.Wrap(err, "job_tosql") - } - - statuses := []*model.Job{} - if err = jss.GetReplicaX().Select(&statuses, query, args...); err != nil { - return nil, errors.Wrap(err, "failed to find Jobs") - } - return statuses, nil -} - -func (jss SqlJobStore) GetAllByTypesPage(jobTypes []string, offset int, limit int) ([]*model.Job, error) { +func (jss SqlJobStore) GetAllByTypesPage(c *request.Context, jobTypes []string, offset int, limit int) ([]*model.Job, error) { query, args, err := jss.getQueryBuilder(). Select("*"). From("Jobs"). @@ -253,10 +240,14 @@ func (jss SqlJobStore) GetAllByTypesPage(jobTypes []string, offset int, limit in if err = jss.GetReplicaX().Select(&jobs, query, args...); err != nil { return nil, errors.Wrapf(err, "failed to find Jobs with types") } + for _, j := range jobs { + j.InitLogger(c.Logger()) + } + return jobs, nil } -func (jss SqlJobStore) GetAllByType(jobType string) ([]*model.Job, error) { +func (jss SqlJobStore) GetAllByType(c *request.Context, jobType string) ([]*model.Job, error) { query, args, err := jss.getQueryBuilder(). Select("*"). From("Jobs"). @@ -265,14 +256,19 @@ func (jss SqlJobStore) GetAllByType(jobType string) ([]*model.Job, error) { if err != nil { return nil, errors.Wrap(err, "job_tosql") } + statuses := []*model.Job{} if err = jss.GetReplicaX().Select(&statuses, query, args...); err != nil { return nil, errors.Wrapf(err, "failed to find Jobs with type=%s", jobType) } + for _, j := range statuses { + j.InitLogger(c.Logger()) + } + return statuses, nil } -func (jss SqlJobStore) GetAllByTypeAndStatus(jobType string, status string) ([]*model.Job, error) { +func (jss SqlJobStore) GetAllByTypeAndStatus(c *request.Context, jobType string, status string) ([]*model.Job, error) { query, args, err := jss.getQueryBuilder(). Select("*"). From("Jobs"). @@ -281,14 +277,19 @@ func (jss SqlJobStore) GetAllByTypeAndStatus(jobType string, status string) ([]* if err != nil { return nil, errors.Wrap(err, "job_tosql") } + jobs := []*model.Job{} if err = jss.GetReplicaX().Select(&jobs, query, args...); err != nil { return nil, errors.Wrapf(err, "failed to find Jobs with type=%s", jobType) } + for _, j := range jobs { + j.InitLogger(c.Logger()) + } + return jobs, nil } -func (jss SqlJobStore) GetAllByTypePage(jobType string, offset int, limit int) ([]*model.Job, error) { +func (jss SqlJobStore) GetAllByTypePage(c *request.Context, jobType string, offset int, limit int) ([]*model.Job, error) { query, args, err := jss.getQueryBuilder(). Select("*"). From("Jobs"). @@ -304,10 +305,14 @@ func (jss SqlJobStore) GetAllByTypePage(jobType string, offset int, limit int) ( if err = jss.GetReplicaX().Select(&statuses, query, args...); err != nil { return nil, errors.Wrapf(err, "failed to find Jobs with type=%s", jobType) } + for _, j := range statuses { + j.InitLogger(c.Logger()) + } + return statuses, nil } -func (jss SqlJobStore) GetAllByStatus(status string) ([]*model.Job, error) { +func (jss SqlJobStore) GetAllByStatus(c *request.Context, status string) ([]*model.Job, error) { statuses := []*model.Job{} query, args, err := jss.getQueryBuilder(). Select("*"). @@ -321,6 +326,10 @@ func (jss SqlJobStore) GetAllByStatus(status string) ([]*model.Job, error) { if err = jss.GetReplicaX().Select(&statuses, query, args...); err != nil { return nil, errors.Wrapf(err, "failed to find Jobs with status=%s", status) } + for _, j := range statuses { + j.InitLogger(c.Logger()) + } + return statuses, nil } diff --git a/server/channels/store/sqlstore/license_store.go b/server/channels/store/sqlstore/license_store.go index 545518224bc..d58d7c27e54 100644 --- a/server/channels/store/sqlstore/license_store.go +++ b/server/channels/store/sqlstore/license_store.go @@ -4,6 +4,8 @@ package sqlstore import ( + "context" + sq "github.com/mattermost/squirrel" "github.com/pkg/errors" @@ -23,46 +25,41 @@ func newSqlLicenseStore(sqlStore *SqlStore) store.LicenseStore { // Save validates and stores the license instance in the database. The Id // and Bytes fields are mandatory. The Bytes field is limited to a maximum -// of 10000 bytes. If the license ID matches an existing license in the -// database it returns the license stored in the database. If not, it saves the -// new database and returns the created license with the CreateAt field -// updated. -func (ls SqlLicenseStore) Save(license *model.LicenseRecord) (*model.LicenseRecord, error) { +// of 10000 bytes. Provided license is saved only if missing. +func (ls SqlLicenseStore) Save(license *model.LicenseRecord) error { license.PreSave() if err := license.IsValid(); err != nil { - return nil, err + return err } + query := ls.getQueryBuilder(). - Select("Id, CreateAt, Bytes"). - From("Licenses"). - Where(sq.Eq{"Id": license.Id}) + Insert("Licenses"). + Columns("Id", "CreateAt", "Bytes"). + Values(license.Id, license.CreateAt, license.Bytes) + + if ls.DriverName() == model.DatabaseDriverMysql { + query = query.SuffixExpr(sq.Expr("ON DUPLICATE KEY UPDATE Id=Id")) + } else { + query = query.SuffixExpr(sq.Expr("ON CONFLICT (Id) DO NOTHING")) + } + queryString, args, err := query.ToSql() if err != nil { - return nil, errors.Wrap(err, "license_tosql") + return errors.Wrap(err, "license_tosql") } - var storedLicense model.LicenseRecord - if err := ls.GetReplicaX().Get(&storedLicense, queryString, args...); err != nil { - // Only insert if not exists - query, args, err := ls.getQueryBuilder(). - Insert("Licenses"). - Columns("Id", "CreateAt", "Bytes"). - Values(license.Id, license.CreateAt, license.Bytes). - ToSql() - if err != nil { - return nil, errors.Wrap(err, "license_record_tosql") - } - if _, err := ls.GetMasterX().Exec(query, args...); err != nil { - return nil, errors.Wrapf(err, "failed to get License with licenseId=%s", license.Id) - } - return license, nil + + if _, err := ls.GetMasterX().Exec(queryString, args...); err != nil { + return errors.Wrapf(err, "failed to insert License with licenseId=%s", license.Id) } - return &storedLicense, nil + + return nil + } // Get obtains the license with the provided id parameter from the database. // If the license doesn't exist it returns a model.AppError with // http.StatusNotFound in the StatusCode field. -func (ls SqlLicenseStore) Get(id string) (*model.LicenseRecord, error) { +func (ls SqlLicenseStore) Get(ctx context.Context, id string) (*model.LicenseRecord, error) { query := ls.getQueryBuilder(). Select("Id, CreateAt, Bytes"). From("Licenses"). @@ -74,7 +71,7 @@ func (ls SqlLicenseStore) Get(id string) (*model.LicenseRecord, error) { } license := &model.LicenseRecord{} - if err := ls.GetReplicaX().Get(license, queryString, args...); err != nil { + if err := ls.DBXFromContext(ctx).Get(license, queryString, args...); err != nil { return nil, store.NewErrNotFound("License", id) } return license, nil diff --git a/server/channels/store/sqlstore/post_store.go b/server/channels/store/sqlstore/post_store.go index f5128f6a706..33303c28c50 100644 --- a/server/channels/store/sqlstore/post_store.go +++ b/server/channels/store/sqlstore/post_store.go @@ -11,6 +11,7 @@ import ( "regexp" "strings" "sync" + "time" sq "github.com/mattermost/squirrel" "github.com/pkg/errors" @@ -929,24 +930,31 @@ func (s *SqlPostStore) Delete(postID string, time int64, deleteByID string) (err return nil } -func (s *SqlPostStore) permanentDelete(postId string) (err error) { - var post model.Post +func (s *SqlPostStore) permanentDelete(postIds []string) (err error) { transaction, err := s.GetMasterX().Beginx() if err != nil { return errors.Wrap(err, "begin_transaction") } defer finalizeTransactionX(transaction, &err) - err = transaction.Get(&post, "SELECT * FROM Posts WHERE Id = ?", postId) - if err != nil && err != sql.ErrNoRows { - return errors.Wrapf(err, "failed to get Post with id=%s", postId) - } - if err = s.permanentDeleteThreads(transaction, post.Id); err != nil { - return errors.Wrapf(err, "failed to cleanup threads for Post with id=%s", postId) + if err = s.permanentDeleteThreads(transaction, postIds); err != nil { + return err } - if _, err = transaction.NamedExec("DELETE FROM Posts WHERE Id = :id OR RootId = :rootid", map[string]any{"id": postId, "rootid": postId}); err != nil { - return errors.Wrapf(err, "failed to delete Post with id=%s", postId) + if err = s.permanentDeleteReactions(transaction, postIds); err != nil { + return err + } + + query := s.getQueryBuilder(). + Delete("Posts"). + Where( + sq.Or{ + sq.Eq{"Id": postIds}, + sq.Eq{"RootId": postIds}, + }, + ) + if _, err = transaction.ExecBuilder(query); err != nil { + return errors.Wrap(err, "failed to delete Posts") } if err = transaction.Commit(); err != nil { @@ -980,10 +988,17 @@ func (s *SqlPostStore) permanentDeleteAllCommentByUser(userId string) (err error return errors.Wrapf(err, "failed to delete Posts with userId=%s", userId) } + postIds := []string{} for _, ids := range results { if err = s.updateThreadAfterReplyDeletion(transaction, ids.RootId, userId); err != nil { return err } + postIds = append(postIds, ids.Id) + } + + // Delete all the reactions on the comments + if err = s.permanentDeleteReactions(transaction, postIds); err != nil { + return err } if err = transaction.Commit(); err != nil { @@ -1005,22 +1020,20 @@ func (s *SqlPostStore) PermanentDeleteByUser(userId string) error { // Now attempt to delete all the root posts for a user. This will also // delete all the comments for each post - found := true count := 0 - - for found { + for { var ids []string err := s.GetMasterX().Select(&ids, "SELECT Id FROM Posts WHERE UserId = ? LIMIT 1000", userId) if err != nil { return errors.Wrapf(err, "failed to find Posts with userId=%s", userId) } - found = false - for _, id := range ids { - found = true - if err = s.permanentDelete(id); err != nil { - return err - } + if len(ids) == 0 { + break + } + + if err = s.permanentDelete(ids); err != nil { + return err } // This is a fail safe, give up if more than 10k messages @@ -1035,6 +1048,7 @@ func (s *SqlPostStore) PermanentDeleteByUser(userId string) error { // Permanent deletes all channel root posts and comments, // deletes all threads and thread memberships +// deletes all reactions // no thread comment cleanup needed, since we are deleting threads and thread memberships func (s *SqlPostStore) PermanentDeleteByChannel(channelId string) (err error) { transaction, err := s.GetMasterX().Beginx() @@ -1043,20 +1057,39 @@ func (s *SqlPostStore) PermanentDeleteByChannel(channelId string) (err error) { } defer finalizeTransactionX(transaction, &err) - results := []postIds{} - err = transaction.Select(&results, "SELECT Id, RootId, UserId FROM Posts WHERE ChannelId = ?", channelId) - if err != nil { - return errors.Wrapf(err, "failed to fetch Posts with channelId=%s", channelId) - } + id := "" + for { + ids := []string{} + err = transaction.Select(&ids, "SELECT Id FROM Posts WHERE ChannelId = ? AND Id > ? ORDER BY Id ASC LIMIT 500", channelId, id) + if err != nil { + return errors.Wrapf(err, "failed to fetch Posts with channelId=%s", channelId) + } - for _, ids := range results { - if err = s.permanentDeleteThreads(transaction, ids.Id); err != nil { + if len(ids) == 0 { + break + } + + id = ids[len(ids)-1] + + if err = s.permanentDeleteThreads(transaction, ids); err != nil { return err } - } + time.Sleep(10 * time.Millisecond) - if _, err = transaction.Exec("DELETE FROM Posts WHERE ChannelId = ?", channelId); err != nil { - return errors.Wrapf(err, "failed to delete Posts with channelId=%s", channelId) + if err = s.permanentDeleteReactions(transaction, ids); err != nil { + return err + } + time.Sleep(10 * time.Millisecond) + + query := s.getQueryBuilder(). + Delete("Posts"). + Where( + sq.Eq{"Id": ids}, + ) + if _, err = transaction.ExecBuilder(query); err != nil { + return errors.Wrap(err, "failed to delete Posts") + } + time.Sleep(10 * time.Millisecond) } if err = transaction.Commit(); err != nil { @@ -2419,48 +2452,10 @@ func (s *SqlPostStore) PermanentDeleteBatchForRetentionPolicies(now, globalPolic NowMillis: now, GlobalPolicyEndTime: globalPolicyEndTime, Limit: limit, + StoreDeletedIds: true, }, s.SqlStore, cursor) } -// DeleteOrphanedRows removes entries from Posts when a corresponding channel no longer exists. -func (s *SqlPostStore) DeleteOrphanedRows(limit int) (deleted int64, err error) { - var query string - // We need the extra level of nesting to deal with MySQL's locking - if s.DriverName() == model.DatabaseDriverMysql { - // MySQL fails to do a proper antijoin if the selecting column - // and the joining column are different. In that case, doing a subquery - // leads to a faster plan because MySQL materializes the sub-query - // and does a covering index scan on Posts table. More details on the PR with - // this commit. - query = ` - DELETE FROM Posts WHERE Id IN ( - SELECT * FROM ( - SELECT Posts.Id FROM Posts - WHERE Posts.ChannelId NOT IN (SELECT Id FROM Channels USE INDEX (PRIMARY)) - LIMIT ? - ) AS A - )` - } else { - query = ` - DELETE FROM Posts WHERE Id IN ( - SELECT * FROM ( - SELECT Posts.Id FROM Posts - LEFT JOIN Channels ON Posts.ChannelId = Channels.Id - WHERE Channels.Id IS NULL - LIMIT ? - ) AS A - )` - - } - - result, err := s.GetMasterX().Exec(query, limit) - if err != nil { - return - } - deleted, err = result.RowsAffected() - return -} - func (s *SqlPostStore) PermanentDeleteBatch(endTime int64, limit int64) (int64, error) { var query string if s.DriverName() == "postgres" { @@ -2556,7 +2551,7 @@ func (s *SqlPostStore) GetMaxPostSize() int { return s.maxPostSizeCached } -func (s *SqlPostStore) GetParentsForExportAfter(limit int, afterId string) ([]*model.PostForExport, error) { +func (s *SqlPostStore) GetParentsForExportAfter(limit int, afterId string, includeArchivedChannel bool) ([]*model.PostForExport, error) { for { rootIds := []string{} err := s.GetReplicaX().Select(&rootIds, @@ -2580,16 +2575,20 @@ func (s *SqlPostStore) GetParentsForExportAfter(limit int, afterId string) ([]*m return postsForExport, nil } + excludeDeletedCond := sq.And{ + sq.Eq{"Teams.DeleteAt": 0}, + } + if !includeArchivedChannel { + excludeDeletedCond = append(excludeDeletedCond, sq.Eq{"Channels.DeleteAt": 0}) + } + builder := s.getQueryBuilder(). Select("p1.*, Users.Username as Username, Teams.Name as TeamName, Channels.Name as ChannelName"). FromSelect(sq.Select("*").From("Posts").Where(sq.Eq{"Posts.Id": rootIds}), "p1"). InnerJoin("Channels ON p1.ChannelId = Channels.Id"). InnerJoin("Teams ON Channels.TeamId = Teams.Id"). InnerJoin("Users ON p1.UserId = Users.Id"). - Where(sq.And{ - sq.Eq{"Channels.DeleteAt": 0}, - sq.Eq{"Teams.DeleteAt": 0}, - }). + Where(excludeDeletedCond). OrderBy("p1.Id") query, args, err := builder.ToSql() @@ -2775,16 +2774,40 @@ func (s *SqlPostStore) GetOldestEntityCreationTime() (int64, error) { } // Deletes a thread and a thread membership if the postId is a root post -func (s *SqlPostStore) permanentDeleteThreads(transaction *sqlxTxWrapper, postId string) error { - if _, err := transaction.Exec("DELETE FROM Threads WHERE PostId = ?", postId); err != nil { +func (s *SqlPostStore) permanentDeleteThreads(transaction *sqlxTxWrapper, postIds []string) error { + query := s.getQueryBuilder(). + Delete("Threads"). + Where( + sq.Eq{"PostId": postIds}, + ) + if _, err := transaction.ExecBuilder(query); err != nil { return errors.Wrap(err, "failed to delete Threads") } - if _, err := transaction.Exec("DELETE FROM ThreadMemberships WHERE PostId = ?", postId); err != nil { + + query = s.getQueryBuilder(). + Delete("ThreadMemberships"). + Where( + sq.Eq{"PostId": postIds}, + ) + if _, err := transaction.ExecBuilder(query); err != nil { return errors.Wrap(err, "failed to delete ThreadMemberships") } return nil } +func (s *SqlPostStore) permanentDeleteReactions(transaction *sqlxTxWrapper, postIds []string) error { + query := s.getQueryBuilder(). + Delete("Reactions"). + Where( + sq.Eq{"PostId": postIds}, + ) + if _, err := transaction.ExecBuilder(query); err != nil { + return errors.Wrap(err, "failed to delete Reactions") + } + + return nil +} + // deleteThread marks a thread as deleted at the given time. func (s *SqlPostStore) deleteThread(transaction *sqlxTxWrapper, postId string, deleteAtTime int64) error { queryString, args, err := s.getQueryBuilder(). diff --git a/server/channels/store/sqlstore/reaction_store.go b/server/channels/store/sqlstore/reaction_store.go index 1d9183cc2f4..19bbeecc9ac 100644 --- a/server/channels/store/sqlstore/reaction_store.go +++ b/server/channels/store/sqlstore/reaction_store.go @@ -4,6 +4,8 @@ package sqlstore import ( + "time" + sq "github.com/mattermost/squirrel" "github.com/mattermost/mattermost/server/public/model" @@ -205,24 +207,91 @@ func (s *SqlReactionStore) DeleteAllWithEmojiName(emojiName string) error { return nil } -// DeleteOrphanedRows removes entries from Reactions when a corresponding post no longer exists. -func (s *SqlReactionStore) DeleteOrphanedRows(limit int) (deleted int64, err error) { - // We need the extra level of nesting to deal with MySQL's locking - const query = ` - DELETE FROM Reactions WHERE PostId IN ( - SELECT * FROM ( - SELECT PostId FROM Reactions - LEFT JOIN Posts ON Reactions.PostId = Posts.Id - WHERE Posts.Id IS NULL - LIMIT ? - ) AS A - )` - result, err := s.GetMasterX().Exec(query, limit) +func (s *SqlReactionStore) permanentDeleteReactions(userId string, postIds *[]string) error { + txn, err := s.GetMasterX().Beginx() if err != nil { - return + return err } - deleted, err = result.RowsAffected() - return + defer finalizeTransactionX(txn, &err) + + err = txn.Select(postIds, "SELECT PostId FROM Reactions WHERE UserId = ?", userId) + if err != nil { + return errors.Wrapf(err, "failed to get Reactions with userId=%s", userId) + } + + query := s.getQueryBuilder(). + Delete("Reactions"). + Where(sq.And{ + sq.Eq{"PostId": postIds}, + sq.Eq{"UserId": userId}, + }) + + _, err = txn.ExecBuilder(query) + if err != nil { + return errors.Wrapf(err, "failed to delete reactions with userId=%s", userId) + } + if err = txn.Commit(); err != nil { + return err + } + return nil +} + +func (s SqlReactionStore) PermanentDeleteByUser(userId string) error { + now := model.GetMillis() + postIds := []string{} + + err := s.permanentDeleteReactions(userId, &postIds) + if err != nil { + return err + } + + transaction, err := s.GetMasterX().Beginx() + if err != nil { + return err + } + defer finalizeTransactionX(transaction, &err) + + for _, postId := range postIds { + _, err = transaction.Exec(UpdatePostHasReactionsOnDeleteQuery, now, postId, postId) + if err != nil { + mlog.Warn("Unable to update Post.HasReactions while removing reactions", + mlog.String("post_id", postId), + mlog.Err(err)) + } + time.Sleep(10 * time.Millisecond) + } + if err = transaction.Commit(); err != nil { + return err + } + + return nil +} + +func (s *SqlReactionStore) DeleteOrphanedRowsByIds(r *model.RetentionIdsForDeletion) error { + txn, err := s.GetMasterX().Beginx() + if err != nil { + return err + } + defer finalizeTransactionX(txn, &err) + + query := s.getQueryBuilder(). + Delete("Reactions"). + Where( + sq.Eq{"PostId": r.Ids}, + ) + + _, err = txn.ExecBuilder(query) + if err != nil { + return errors.Wrapf(err, "failed to delete orphaned reactions with RetentionIdsForDeletion Id=%s", r.Id) + } + err = deleteFromRetentionIdsTx(txn, r.Id) + if err != nil { + return err + } + if err = txn.Commit(); err != nil { + return err + } + return nil } func (s *SqlReactionStore) PermanentDeleteBatch(endTime int64, limit int64) (int64, error) { diff --git a/server/channels/store/sqlstore/retention_policy_store.go b/server/channels/store/sqlstore/retention_policy_store.go index 4ddc4a79def..b10a87f2213 100644 --- a/server/channels/store/sqlstore/retention_policy_store.go +++ b/server/channels/store/sqlstore/retention_policy_store.go @@ -5,6 +5,7 @@ package sqlstore import ( "database/sql" + "encoding/json" "fmt" "strconv" "strings" @@ -817,6 +818,92 @@ func (s *SqlRetentionPolicyStore) GetChannelPoliciesCountForUser(userID string) return count, nil } +func scanRetentionIdsForDeletion(rows *sql.Rows, isPostgres bool) ([]*model.RetentionIdsForDeletion, error) { + idsForDeletion := []*model.RetentionIdsForDeletion{} + for rows.Next() { + var row model.RetentionIdsForDeletion + if isPostgres { + if err := rows.Scan( + &row.Id, &row.TableName, pq.Array(&row.Ids), + ); err != nil { + return nil, errors.Wrap(err, "unable to scan columns") + } + } else { + var ids []byte + if err := rows.Scan( + &row.Id, &row.TableName, &ids, + ); err != nil { + return nil, errors.Wrap(err, "unable to scan columns") + } + if err := json.Unmarshal(ids, &row.Ids); err != nil { + return nil, errors.Wrap(err, "failed to unmarshal ids") + } + } + + idsForDeletion = append(idsForDeletion, &row) + } + if err := rows.Err(); err != nil { + return nil, errors.Wrap(err, "error while iterating over rows") + } + return idsForDeletion, nil +} + +func (s *SqlRetentionPolicyStore) GetIdsForDeletionByTableName(tableName string, limit int) ([]*model.RetentionIdsForDeletion, error) { + query := s.getQueryBuilder(). + Select("*"). + From("RetentionIdsForDeletion"). + Where( + sq.Eq{"TableName": tableName}, + ). + Limit(uint64(limit)) + + queryString, args, err := query.ToSql() + if err != nil { + return nil, errors.Wrap(err, "get_ids_for_deletion_tosql") + } + + rows, err := s.GetReplicaX().DB.Query(queryString, args...) + if err != nil { + return nil, errors.Wrap(err, "failed to get ids for deletion") + } + defer rows.Close() + + isPostgres := s.DriverName() == model.DatabaseDriverPostgres + idsForDeletion, err := scanRetentionIdsForDeletion(rows, isPostgres) + if err != nil { + return nil, errors.Wrap(err, "failed to scan ids for deletion") + } + + return idsForDeletion, nil +} + +func insertRetentionIdsForDeletion(txn *sqlxTxWrapper, row *model.RetentionIdsForDeletion, s *SqlStore) error { + row.PreSave() + insertBuilder := s.getQueryBuilder(). + Insert("RetentionIdsForDeletion"). + Columns("Id", "TableName", "Ids") + if s.DriverName() == model.DatabaseDriverPostgres { + insertBuilder = insertBuilder. + Values(row.Id, row.TableName, pq.Array(row.Ids)) + } else { + jsonIds, err := json.Marshal(row.Ids) + if err != nil { + return err + } + insertBuilder = insertBuilder. + Values(row.Id, row.TableName, jsonIds) + } + insertQuery, insertArgs, err := insertBuilder.ToSql() + if err != nil { + return err + } + + if _, err = txn.Exec(insertQuery, insertArgs...); err != nil { + return err + } + return nil +} + // RetentionPolicyBatchDeletionInfo gives information on how to delete records // under a retention policy; see `genericPermanentDeleteBatchForRetentionPolicies`. // @@ -845,6 +932,7 @@ type RetentionPolicyBatchDeletionInfo struct { NowMillis int64 GlobalPolicyEndTime int64 Limit int64 + StoreDeletedIds bool } // genericPermanentDeleteBatchForRetentionPolicies is a helper function for tables @@ -958,31 +1046,115 @@ func genericRetentionPoliciesDeletion( if err != nil { return 0, errors.Wrap(err, r.Table+"_tosql") } - if s.DriverName() == model.DatabaseDriverPostgres { - primaryKeysStr := "(" + strings.Join(r.PrimaryKeys, ",") + ")" - query = ` - DELETE FROM ` + r.Table + ` WHERE ` + primaryKeysStr + ` IN ( - ` + query + ` - )` + + if r.StoreDeletedIds { + txn, err := s.GetMasterX().Beginx() + if err != nil { + return 0, err + } + defer finalizeTransactionX(txn, &err) + + if s.DriverName() == model.DatabaseDriverPostgres { + primaryKeysStr := "(" + strings.Join(r.PrimaryKeys, ",") + ")" + + query = fmt.Sprintf("DELETE FROM %s WHERE %s IN (%s) RETURNING %s.%s", r.Table, primaryKeysStr, query, r.Table, r.PrimaryKeys[0]) + var rows *sql.Rows + rows, err = txn.Query(query, args...) + if err != nil { + return 0, errors.Wrap(err, "failed to delete "+r.Table) + } + + defer rows.Close() + ids := []string{} + for rows.Next() { + var id string + if err = rows.Scan(&id); err != nil { + return 0, errors.Wrap(err, "unable to scan from rows") + } + ids = append(ids, id) + } + if err = rows.Err(); err != nil { + return 0, errors.Wrap(err, "failed while iterating over rows") + } + rowsAffected = int64(len(ids)) + + if len(ids) > 0 { + retentionIdsRow := model.RetentionIdsForDeletion{ + TableName: r.Table, + Ids: ids, + } + err = insertRetentionIdsForDeletion(txn, &retentionIdsRow, s) + if err != nil { + return 0, err + } + } + } else { + retentionIdsRow := model.RetentionIdsForDeletion{ + TableName: r.Table, + Ids: []string{}, + } + // 1. Select rows that will be deleted + if err = txn.Select(&retentionIdsRow.Ids, query, args...); err != nil { + return 0, err + } + + if len(retentionIdsRow.Ids) > 0 { + // 2. Insert selected ids into RetentionIdsForDeletion table + err = insertRetentionIdsForDeletion(txn, &retentionIdsRow, s) + if err != nil { + return 0, err + } + + query = getDeleteQueriesForMySQL(r, query) + + // 3. Delete from Parent table + var result sql.Result + result, err = txn.Exec(query, args...) + if err != nil { + return 0, errors.Wrap(err, "failed to delete "+r.Table) + } + + rowsAffected, err = result.RowsAffected() + if err != nil { + return 0, errors.Wrap(err, "failed to get rows affected for "+r.Table) + } + } + } + if err = txn.Commit(); err != nil { + return 0, err + } } else { - // MySQL does not support the LIMIT clause in a subquery with IN - clauses := make([]string, len(r.PrimaryKeys)) - for i, key := range r.PrimaryKeys { - clauses[i] = r.Table + "." + key + " = A." + key + if s.DriverName() == model.DatabaseDriverPostgres { + primaryKeysStr := "(" + strings.Join(r.PrimaryKeys, ",") + ")" + query = fmt.Sprintf("DELETE FROM %s WHERE %s IN (%s)", r.Table, primaryKeysStr, query) + } else { + query = getDeleteQueriesForMySQL(r, query) + } + result, err := s.GetMasterX().Exec(query, args...) + if err != nil { + return 0, errors.Wrap(err, "failed to delete "+r.Table) + } + rowsAffected, err = result.RowsAffected() + if err != nil { + return 0, errors.Wrap(err, "failed to get rows affected for "+r.Table) } - joinClause := strings.Join(clauses, " AND ") - query = ` - DELETE ` + r.Table + ` FROM ` + r.Table + ` INNER JOIN ( - ` + query + ` - ) AS A ON ` + joinClause - } - result, err := s.GetMasterX().Exec(query, args...) - if err != nil { - return 0, errors.Wrap(err, "failed to delete "+r.Table) - } - rowsAffected, err = result.RowsAffected() - if err != nil { - return 0, errors.Wrap(err, "failed to get rows affected for "+r.Table) } return } + +func getDeleteQueriesForMySQL(r RetentionPolicyBatchDeletionInfo, query string) string { + // MySQL does not support the LIMIT clause in a subquery with IN + clauses := make([]string, len(r.PrimaryKeys)) + for i, key := range r.PrimaryKeys { + clauses[i] = r.Table + "." + key + " = A." + key + } + joinClause := strings.Join(clauses, " AND ") + return fmt.Sprintf("DELETE %s FROM %s INNER JOIN (%s) AS A ON %s", r.Table, r.Table, query, joinClause) +} + +func deleteFromRetentionIdsTx(txn *sqlxTxWrapper, id string) (err error) { + if _, err := txn.Exec("DELETE FROM RetentionIdsForDeletion WHERE Id = ?", id); err != nil { + return errors.Wrap(err, "Failed to delete from RetentionIdsForDeletion") + } + return nil +} diff --git a/server/channels/store/sqlstore/store.go b/server/channels/store/sqlstore/store.go index 200c34fa2cf..03a008bf14a 100644 --- a/server/channels/store/sqlstore/store.go +++ b/server/channels/store/sqlstore/store.go @@ -108,6 +108,7 @@ type SqlStoreStores struct { postAcknowledgement store.PostAcknowledgementStore postPersistentNotification store.PostPersistentNotificationStore trueUpReview store.TrueUpReviewStore + desktopTokens store.DesktopTokensStore } type SqlStore struct { @@ -229,6 +230,7 @@ func New(settings model.SqlSettings, metrics einterfaces.MetricsInterface) (*Sql store.stores.postAcknowledgement = newSqlPostAcknowledgementStore(store) store.stores.postPersistentNotification = newSqlPostPersistentNotificationStore(store) store.stores.trueUpReview = newSqlTrueUpReviewStore(store) + store.stores.desktopTokens = newSqlDesktopTokensStore(store, metrics) store.stores.preference.(*SqlPreferenceStore).deleteUnusedFeatures() @@ -1080,6 +1082,10 @@ func (ss *SqlStore) TrueUpReview() store.TrueUpReviewStore { return ss.stores.trueUpReview } +func (ss *SqlStore) DesktopTokens() store.DesktopTokensStore { + return ss.stores.desktopTokens +} + func (ss *SqlStore) DropAllTables() { if ss.DriverName() == model.DatabaseDriverPostgres { ss.masterX.Exec(`DO diff --git a/server/channels/store/sqlstore/team_store.go b/server/channels/store/sqlstore/team_store.go index 146dd3b230e..e13d1fc19c4 100644 --- a/server/channels/store/sqlstore/team_store.go +++ b/server/channels/store/sqlstore/team_store.go @@ -1513,6 +1513,46 @@ func (s SqlTeamStore) GetCommonTeamIDsForTwoUsers(userID, otherUserID string) ([ return teamIDs, nil } +func (s SqlTeamStore) GetCommonTeamIDsForMultipleUsers(userIDs []string) ([]string, error) { + subQuery := s.getSubQueryBuilder(). + Select("TeamId, UserId"). + From("TeamMembers"). + Where(sq.Eq{ + "UserId": userIDs, + "DeleteAt": 0, + }) + + subQuerySQL, subQueryParams, err := subQuery.ToSql() + if err != nil { + return nil, errors.Wrap(err, "GetCommonTeamIDsForMultipleUsers_subquery_toSQL") + } + + query := s.getQueryBuilder(). + Select("t.Id"). + From("Teams AS t"). + Join("("+subQuerySQL+") AS tm ON t.Id = tm.TeamId", subQueryParams...). + Where(sq.Eq{ + "t.DeleteAt": 0, + }). + GroupBy("t.Id"). + Having(sq.Eq{ + "COUNT(UserId)": len(userIDs), + }) + + querySQL, args, err := query.ToSql() + if err != nil { + return nil, errors.Wrap(err, "GetCommonTeamIDsForMultipleUsers_query_toSQL") + } + + var teamIDs []string + + if err := s.GetReplicaX().Select(&teamIDs, querySQL, args...); err != nil { + return nil, errors.Wrapf(err, "failed to find common team for users %v", userIDs) + } + + return teamIDs, nil +} + // GetTeamMembersForExport gets the various teams for which a user, denoted by userId, is a part of. func (s SqlTeamStore) GetTeamMembersForExport(userId string) ([]*model.TeamMemberForExport, error) { members := []*model.TeamMemberForExport{} diff --git a/server/channels/store/sqlstore/thread_store.go b/server/channels/store/sqlstore/thread_store.go index 8a7627a6de5..6672d12220f 100644 --- a/server/channels/store/sqlstore/thread_store.go +++ b/server/channels/store/sqlstore/thread_store.go @@ -919,6 +919,7 @@ func (s *SqlThreadStore) PermanentDeleteBatchForRetentionPolicies(now, globalPol NowMillis: now, GlobalPolicyEndTime: globalPolicyEndTime, Limit: limit, + StoreDeletedIds: false, }, s.SqlStore, cursor) } @@ -939,66 +940,32 @@ func (s *SqlThreadStore) PermanentDeleteBatchThreadMembershipsForRetentionPolici NowMillis: now, GlobalPolicyEndTime: globalPolicyEndTime, Limit: limit, + StoreDeletedIds: false, }, s.SqlStore, cursor) } // DeleteOrphanedRows removes orphaned rows from Threads and ThreadMemberships func (s *SqlThreadStore) DeleteOrphanedRows(limit int) (deleted int64, err error) { - var threadsQuery string - // We need the extra level of nesting to deal with MySQL's locking - if s.DriverName() == model.DatabaseDriverMysql { - // MySQL fails to do a proper antijoin if the selecting column - // and the joining column are different. In that case, doing a subquery - // leads to a faster plan because MySQL materializes the sub-query - // and does a covering index scan on Threads table. More details on the PR with - // this commit. - threadsQuery = ` - DELETE FROM Threads WHERE PostId IN ( - SELECT * FROM ( - SELECT Threads.PostId FROM Threads - WHERE Threads.ChannelId NOT IN (SELECT Id FROM Channels USE INDEX(PRIMARY)) - LIMIT ? - ) AS A - )` - } else { - threadsQuery = ` - DELETE FROM Threads WHERE PostId IN ( - SELECT * FROM ( - SELECT Threads.PostId FROM Threads - LEFT JOIN Channels ON Threads.ChannelId = Channels.Id - WHERE Channels.Id IS NULL - LIMIT ? - ) AS A - )` - } // We only delete a thread membership if the entire thread no longer exists, // not if the root post has been deleted const threadMembershipsQuery = ` - DELETE FROM ThreadMemberships WHERE PostId IN ( - SELECT * FROM ( - SELECT ThreadMemberships.PostId FROM ThreadMemberships - LEFT JOIN Threads ON ThreadMemberships.PostId = Threads.PostId - WHERE Threads.PostId IS NULL - LIMIT ? - ) AS A - )` - result, err := s.GetMasterX().Exec(threadsQuery, limit) + DELETE FROM ThreadMemberships WHERE PostId IN ( + SELECT * FROM ( + SELECT ThreadMemberships.PostId FROM ThreadMemberships + LEFT JOIN Threads ON ThreadMemberships.PostId = Threads.PostId + WHERE Threads.PostId IS NULL + LIMIT ? + ) AS A + )` + + result, err := s.GetMasterX().Exec(threadMembershipsQuery, limit) if err != nil { return } - rpcDeleted, err := result.RowsAffected() + deleted, err = result.RowsAffected() if err != nil { return } - result, err = s.GetMasterX().Exec(threadMembershipsQuery, limit) - if err != nil { - return - } - rptDeleted, err := result.RowsAffected() - if err != nil { - return - } - deleted = rpcDeleted + rptDeleted return } diff --git a/server/channels/store/sqlstore/user_store.go b/server/channels/store/sqlstore/user_store.go index 729fc151520..e59d81e6483 100644 --- a/server/channels/store/sqlstore/user_store.go +++ b/server/channels/store/sqlstore/user_store.go @@ -1174,6 +1174,26 @@ func (us SqlUserStore) GetByEmail(email string) (*model.User, error) { return &user, nil } +func (us SqlUserStore) GetByRemoteID(remoteID string) (*model.User, error) { + query := us.usersQuery.Where(sq.Eq{"RemoteId": remoteID}) + + queryString, args, err := query.ToSql() + if err != nil { + return nil, errors.Wrap(err, "get_by_remote_id_tosql") + } + + user := model.User{} + if err := us.GetReplicaX().Get(&user, queryString, args...); err != nil { + if err == sql.ErrNoRows { + return nil, errors.Wrap(store.NewErrNotFound("User", fmt.Sprintf("remoteid=%s", remoteID)), "failed to find User") + } + + return nil, errors.Wrapf(err, "failed to get User with RemoteId=%s", remoteID) + } + + return &user, nil +} + func (us SqlUserStore) GetByAuth(authData *string, authService string) (*model.User, error) { if authData == nil || *authData == "" { return nil, store.NewErrInvalidInput("User", "", "empty or nil") @@ -1310,6 +1330,10 @@ func (us SqlUserStore) Count(options model.UserCountOptions) (int64, error) { query = query.Where("u.DeleteAt = 0") } + if !options.IncludeRemoteUsers { + query = query.Where(sq.Or{sq.Eq{"u.RemoteId": ""}, sq.Eq{"u.RemoteId": nil}}) + } + isPostgreSQL := us.DriverName() == model.DatabaseDriverPostgres if options.IncludeBotAccounts { if options.ExcludeRegularUsers { @@ -1365,8 +1389,17 @@ func (us SqlUserStore) AnalyticsActiveCount(timePeriod int64, options model.User query = query.Where(sq.Expr("UserId NOT IN (SELECT UserId FROM Bots)")) } } + + if !options.IncludeRemoteUsers || !options.IncludeDeleted { + query = query.LeftJoin("Users ON s.UserId = Users.Id") + } + + if !options.IncludeRemoteUsers { + query = query.Where(sq.Or{sq.Eq{"Users.RemoteId": ""}, sq.Eq{"Users.RemoteId": nil}}) + } + if !options.IncludeDeleted { - query = query.LeftJoin("Users ON s.UserId = Users.Id").Where("Users.DeleteAt = 0") + query = query.Where("Users.DeleteAt = 0") } queryStr, args, err := query.ToSql() @@ -1393,8 +1426,16 @@ func (us SqlUserStore) AnalyticsActiveCountForPeriod(startTime int64, endTime in } } + if !options.IncludeRemoteUsers || !options.IncludeDeleted { + query = query.LeftJoin("Users ON s.UserId = Users.Id") + } + + if !options.IncludeRemoteUsers { + query = query.Where(sq.Or{sq.Eq{"Users.RemoteId": ""}, sq.Eq{"Users.RemoteId": nil}}) + } + if !options.IncludeDeleted { - query = query.LeftJoin("Users ON s.UserId = Users.Id").Where("Users.DeleteAt = 0") + query = query.Where("Users.DeleteAt = 0") } queryStr, args, err := query.ToSql() diff --git a/server/channels/store/store.go b/server/channels/store/store.go index d6611e6f232..0c66704e0ef 100644 --- a/server/channels/store/store.go +++ b/server/channels/store/store.go @@ -11,6 +11,7 @@ import ( "time" "github.com/mattermost/mattermost/server/public/model" + "github.com/mattermost/mattermost/server/public/shared/request" "github.com/mattermost/mattermost/server/v8/channels/product" ) @@ -87,6 +88,7 @@ type Store interface { PostAcknowledgement() PostAcknowledgementStore PostPersistentNotification() PostPersistentNotificationStore TrueUpReview() TrueUpReviewStore + DesktopTokens() DesktopTokensStore } type RetentionPolicyStore interface { @@ -109,6 +111,7 @@ type RetentionPolicyStore interface { GetTeamPoliciesCountForUser(userID string) (int64, error) GetChannelPoliciesForUser(userID string, offset, limit int) ([]*model.RetentionPolicyForChannel, error) GetChannelPoliciesCountForUser(userID string) (int64, error) + GetIdsForDeletionByTableName(tableName string, limit int) ([]*model.RetentionIdsForDeletion, error) } type TeamStore interface { @@ -171,6 +174,8 @@ type TeamStore interface { // GetCommonTeamIDsForTwoUsers returns the intersection of all the teams to which the specified // users belong. GetCommonTeamIDsForTwoUsers(userID, otherUserID string) ([]string, error) + + GetCommonTeamIDsForMultipleUsers(userIDs []string) ([]string, error) } type ChannelStore interface { @@ -192,6 +197,7 @@ type ChannelStore interface { GetByName(team_id string, name string, allowFromCache bool) (*model.Channel, error) GetByNames(team_id string, names []string, allowFromCache bool) ([]*model.Channel, error) GetByNameIncludeDeleted(team_id string, name string, allowFromCache bool) (*model.Channel, error) + GetByNamesIncludeDeleted(team_id string, names []string, allowFromCache bool) ([]*model.Channel, error) GetDeletedByName(team_id string, name string) (*model.Channel, error) GetDeleted(team_id string, offset int, limit int, userID string) (model.ChannelList, error) GetChannels(teamID, userID string, opts *model.ChannelSearchOpts) (model.ChannelList, error) @@ -265,6 +271,7 @@ type ChannelStore interface { GetMembersInfoByChannelIds(channelIDs []string) (map[string][]*model.User, error) AnalyticsDeletedTypeCount(teamID string, channelType model.ChannelType) (int64, error) GetChannelUnread(channelID, userID string) (*model.ChannelUnread, error) + GetChannelsWithUnreadsAndWithMentions(ctx context.Context, channelIDs []string, userID string, userNotifyProps model.StringMap) ([]string, []string, map[string]int64, error) ClearCaches() ClearMembersForUserCache() GetChannelsByScheme(schemeID string, offset int, limit int) (model.ChannelList, error) @@ -282,9 +289,10 @@ type ChannelStore interface { UpdateSidebarChannelsByPreferences(preferences model.Preferences) error DeleteSidebarChannelsByPreferences(preferences model.Preferences) error DeleteSidebarCategory(categoryID string) error + DeleteAllSidebarChannelForChannel(channelID string) error GetAllChannelsForExportAfter(limit int, afterID string) ([]*model.ChannelForExport, error) GetAllDirectChannelsForExportAfter(limit int, afterID string) ([]*model.DirectChannelForExport, error) - GetChannelMembersForExport(userID string, teamID string) ([]*model.ChannelMemberForExport, error) + GetChannelMembersForExport(userID string, teamID string, includeArchivedChannel bool) ([]*model.ChannelMemberForExport, error) RemoveAllDeactivatedMembers(channelID string) error GetChannelsBatchForIndexing(startTime int64, startChannelID string, limit int) ([]*model.Channel, error) UserBelongsToChannels(userID string, channelIds []string) (bool, error) @@ -374,11 +382,10 @@ type PostStore interface { GetEditHistoryForPost(postId string) ([]*model.Post, error) GetPostsBatchForIndexing(startTime int64, startPostID string, limit int) ([]*model.PostForIndexing, error) PermanentDeleteBatchForRetentionPolicies(now, globalPolicyEndTime, limit int64, cursor model.RetentionPolicyCursor) (int64, model.RetentionPolicyCursor, error) - DeleteOrphanedRows(limit int) (deleted int64, err error) PermanentDeleteBatch(endTime int64, limit int64) (int64, error) GetOldest() (*model.Post, error) GetMaxPostSize() int - GetParentsForExportAfter(limit int, afterID string) ([]*model.PostForExport, error) + GetParentsForExportAfter(limit int, afterID string, includeArchivedChannels bool) ([]*model.PostForExport, error) GetRepliesForExport(parentID string) ([]*model.ReplyForExport, error) GetDirectPostParentsForExportAfter(limit int, afterID string) ([]*model.DirectPostForExport, error) SearchPostsForUser(paramsList []*model.SearchParams, userID, teamID string, page, perPage int) (*model.PostSearchResults, error) @@ -423,6 +430,7 @@ type UserStore interface { GetProfileByGroupChannelIdsForUser(userID string, channelIds []string) (map[string][]*model.User, error) InvalidateProfileCacheForUser(userID string) GetByEmail(email string) (*model.User, error) + GetByRemoteID(remoteID string) (*model.User, error) GetByAuth(authData *string, authService string) (*model.User, error) GetAllUsingAuthService(authService string) ([]*model.User, error) GetAllNotInAuthService(authServices []string) ([]*model.User, error) @@ -633,8 +641,8 @@ type PreferenceStore interface { } type LicenseStore interface { - Save(license *model.LicenseRecord) (*model.LicenseRecord, error) - Get(id string) (*model.LicenseRecord, error) + Save(license *model.LicenseRecord) error + Get(ctx context.Context, id string) (*model.LicenseRecord, error) GetAll() ([]*model.LicenseRecord, error) } @@ -647,11 +655,19 @@ type TokenStore interface { RemoveAllTokensByType(tokenType string) error } +type DesktopTokensStore interface { + GetUserId(token string, minCreatedAt int64) (*string, error) + Insert(token string, createAt int64, userId string) error + Delete(token string) error + DeleteByUserId(userId string) error + DeleteOlderThan(minCreatedAt int64) error +} + type EmojiStore interface { Save(emoji *model.Emoji) (*model.Emoji, error) - Get(ctx context.Context, id string, allowFromCache bool) (*model.Emoji, error) - GetByName(ctx context.Context, name string, allowFromCache bool) (*model.Emoji, error) - GetMultipleByName(ctx context.Context, names []string) ([]*model.Emoji, error) + Get(ctx request.CTX, id string, allowFromCache bool) (*model.Emoji, error) + GetByName(ctx request.CTX, name string, allowFromCache bool) (*model.Emoji, error) + GetMultipleByName(ctx request.CTX, names []string) ([]*model.Emoji, error) GetList(offset, limit int, sort string) ([]*model.Emoji, error) Delete(emoji *model.Emoji, timestamp int64) error Search(name string, prefixOnly bool, limit int) ([]*model.Emoji, error) @@ -708,8 +724,9 @@ type ReactionStore interface { GetForPostSince(postId string, since int64, excludeRemoteId string, inclDeleted bool) ([]*model.Reaction, error) DeleteAllWithEmojiName(emojiName string) error BulkGetForPosts(postIds []string) ([]*model.Reaction, error) - DeleteOrphanedRows(limit int) (int64, error) + DeleteOrphanedRowsByIds(r *model.RetentionIdsForDeletion) error PermanentDeleteBatch(endTime int64, limit int64) (int64, error) + PermanentDeleteByUser(userID string) error } type JobStore interface { @@ -721,13 +738,12 @@ type JobStore interface { UpdateOptimistically(job *model.Job, currentStatus string) (bool, error) UpdateStatus(id string, status string) (*model.Job, error) UpdateStatusOptimistically(id string, currentStatus string, newStatus string) (bool, error) - Get(id string) (*model.Job, error) - GetAllPage(offset int, limit int) ([]*model.Job, error) - GetAllByType(jobType string) ([]*model.Job, error) - GetAllByTypeAndStatus(jobType string, status string) ([]*model.Job, error) - GetAllByTypePage(jobType string, offset int, limit int) ([]*model.Job, error) - GetAllByTypesPage(jobTypes []string, offset int, limit int) ([]*model.Job, error) - GetAllByStatus(status string) ([]*model.Job, error) + Get(c *request.Context, id string) (*model.Job, error) + GetAllByType(c *request.Context, jobType string) ([]*model.Job, error) + GetAllByTypeAndStatus(c *request.Context, jobType string, status string) ([]*model.Job, error) + GetAllByTypePage(c *request.Context, jobType string, offset int, limit int) ([]*model.Job, error) + GetAllByTypesPage(c *request.Context, jobTypes []string, offset int, limit int) ([]*model.Job, error) + GetAllByStatus(c *request.Context, status string) ([]*model.Job, error) GetNewestJobByStatusAndType(status string, jobType string) (*model.Job, error) GetNewestJobByStatusesAndType(statuses []string, jobType string) (*model.Job, error) GetCountByStatusAndType(status string, jobType string) (int64, error) @@ -1087,6 +1103,7 @@ type PostReminderMetadata struct { type SidebarCategorySearchOpts struct { TeamID string ExcludeTeam bool + Type model.SidebarCategoryType } // Ensure store service adapter implements `product.StoreService` diff --git a/server/channels/store/storetest/channel_member_history_store.go b/server/channels/store/storetest/channel_member_history_store.go index fdae4aaf455..825a2b3acd2 100644 --- a/server/channels/store/storetest/channel_member_history_store.go +++ b/server/channels/store/storetest/channel_member_history_store.go @@ -389,6 +389,9 @@ func testPermanentDeleteBatchForRetentionPolicies(t *testing.T, ss store.Store) result, err := ss.ChannelMemberHistory().GetUsersInChannelDuring(joinTime, leaveTime, channel.Id) require.NoError(t, err) require.Empty(t, result, "history should have been deleted by channel policy") + rows, err := ss.RetentionPolicy().GetIdsForDeletionByTableName("ChannelMemberHistory", 1000) + require.NoError(t, err) + require.Equal(t, 0, len(rows)) } func testGetChannelsLeftSince(t *testing.T, ss store.Store) { diff --git a/server/channels/store/storetest/channel_store.go b/server/channels/store/storetest/channel_store.go index 70c27e9e954..8823a000412 100644 --- a/server/channels/store/storetest/channel_store.go +++ b/server/channels/store/storetest/channel_store.go @@ -8,6 +8,7 @@ import ( "database/sql" "encoding/json" "errors" + "fmt" "sort" "strconv" "strings" @@ -80,6 +81,7 @@ func TestChannelStore(t *testing.T, ss store.Store, s SqlStore) { t.Run("Delete", func(t *testing.T) { testChannelStoreDelete(t, ss) }) t.Run("GetByName", func(t *testing.T) { testChannelStoreGetByName(t, ss) }) t.Run("GetByNames", func(t *testing.T) { testChannelStoreGetByNames(t, ss) }) + t.Run("GetByNamesIncludeDeleted", func(t *testing.T) { testChannelStoreGetByNamesIncludeDeleted(t, ss) }) t.Run("GetDeletedByName", func(t *testing.T) { testChannelStoreGetDeletedByName(t, ss) }) t.Run("GetDeleted", func(t *testing.T) { testChannelStoreGetDeleted(t, ss) }) t.Run("ChannelMemberStore", func(t *testing.T) { testChannelMemberStore(t, ss) }) @@ -149,6 +151,7 @@ func TestChannelStore(t *testing.T, ss store.Store, s SqlStore) { t.Run("UpdateSidebarChannelsByPreferences", func(t *testing.T) { testUpdateSidebarChannelsByPreferences(t, ss) }) t.Run("SetShared", func(t *testing.T) { testSetShared(t, ss) }) t.Run("GetTeamForChannel", func(t *testing.T) { testGetTeamForChannel(t, ss) }) + t.Run("GetChannelsWithUnreadsAndWithMentions", func(t *testing.T) { testGetChannelsWithUnreadsAndWithMentions(t, ss) }) } func testChannelStoreSave(t *testing.T, ss store.Store) { @@ -857,6 +860,17 @@ func testChannelStoreGetByNames(t *testing.T, ss store.Store) { _, nErr = ss.Channel().Save(&o2, -1) require.NoError(t, nErr) + o3 := model.Channel{ + TeamId: o1.TeamId, + DisplayName: "Name", + Name: NewTestId(), + Type: model.ChannelTypeOpen, + } + _, nErr = ss.Channel().Save(&o3, -1) + require.NoError(t, nErr) + nErr = ss.Channel().Delete(o3.Id, model.GetMillis()) + require.NoError(t, nErr) + for index, tc := range []struct { TeamId string Names []string @@ -893,6 +907,53 @@ func testChannelStoreGetByNames(t *testing.T, ss store.Store) { assert.Empty(t, channels) } +func testChannelStoreGetByNamesIncludeDeleted(t *testing.T, ss store.Store) { + o1 := model.Channel{ + TeamId: model.NewId(), + DisplayName: "Name", + Name: NewTestId(), + Type: model.ChannelTypeOpen, + } + _, nErr := ss.Channel().Save(&o1, -1) + require.NoError(t, nErr) + + o2 := model.Channel{ + TeamId: o1.TeamId, + DisplayName: "Name", + Name: NewTestId(), + Type: model.ChannelTypeOpen, + } + _, nErr = ss.Channel().Save(&o2, -1) + require.NoError(t, nErr) + nErr = ss.Channel().Delete(o2.Id, model.GetMillis()) + require.NoError(t, nErr, "channel should have been deleted") + + for index, tc := range []struct { + TeamId string + Names []string + ExpectedIds []string + }{ + {o1.TeamId, []string{o1.Name}, []string{o1.Id}}, + {o1.TeamId, []string{o1.Name, o2.Name}, []string{o1.Id, o2.Id}}, + {o1.TeamId, nil, nil}, + {o1.TeamId, []string{"foo"}, nil}, + {o1.TeamId, []string{o1.Name, "foo", o2.Name, o2.Name}, []string{o1.Id, o2.Id}}, + {"", []string{o1.Name, "foo", o2.Name, o2.Name}, []string{o1.Id, o2.Id}}, + {"asd", []string{o1.Name, "foo", o2.Name, o2.Name}, nil}, + } { + var channels []*model.Channel + channels, err := ss.Channel().GetByNamesIncludeDeleted(tc.TeamId, tc.Names, true) + require.NoError(t, err) + var ids []string + for _, channel := range channels { + ids = append(ids, channel.Id) + } + sort.Strings(ids) + sort.Strings(tc.ExpectedIds) + assert.Equal(t, tc.ExpectedIds, ids, "tc %v", index) + } +} + func testChannelStoreGetDeletedByName(t *testing.T, ss store.Store) { o1 := &model.Channel{} o1.TeamId = model.NewId() @@ -7646,7 +7707,7 @@ func testChannelStoreGetChannelMembersForExport(t *testing.T, ss store.Store) { _, err = ss.Channel().SaveMember(&m2) require.NoError(t, err) - d1, err := ss.Channel().GetChannelMembersForExport(u1.Id, t1.Id) + d1, err := ss.Channel().GetChannelMembersForExport(u1.Id, t1.Id, false) assert.NoError(t, err) assert.Len(t, d1, 1) @@ -8065,3 +8126,166 @@ func testGetTeamForChannel(t *testing.T, ss store.Store) { var nfErr *store.ErrNotFound require.True(t, errors.As(err, &nfErr)) } + +func testGetChannelsWithUnreadsAndWithMentions(t *testing.T, ss store.Store) { + setupMembership := func( + pushProp string, + withUnreads bool, + withMentions bool, + isDirect bool, + userId string, + ) (model.Channel, model.ChannelMember) { + if !isDirect { + o1 := model.Channel{} + o1.TeamId = model.NewId() + o1.DisplayName = "Channel1" + o1.Name = NewTestId() + o1.Type = model.ChannelTypeOpen + o1.TotalMsgCount = 25 + o1.LastPostAt = 12345 + o1.LastRootPostAt = 12345 + _, nErr := ss.Channel().Save(&o1, -1) + require.NoError(t, nErr) + + m1 := model.ChannelMember{} + m1.ChannelId = o1.Id + m1.UserId = userId + m1.NotifyProps = model.GetDefaultChannelNotifyProps() + m1.NotifyProps[model.PushNotifyProp] = pushProp + if !withUnreads { + m1.MsgCount = o1.TotalMsgCount + m1.LastViewedAt = o1.LastPostAt + } + if withMentions { + m1.MentionCount = 5 + } + _, err := ss.Channel().SaveMember(&m1) + require.NoError(t, err) + + return o1, m1 + } + + o1, err := ss.Channel().CreateDirectChannel(&model.User{Id: userId}, &model.User{Id: model.NewId()}, func(channel *model.Channel) { + channel.TotalMsgCount = 25 + channel.LastPostAt = 12345 + channel.LastRootPostAt = 12345 + }) + require.NoError(t, err) + + m1, err := ss.Channel().GetMember(context.Background(), o1.Id, userId) + require.NoError(t, err) + + if !withUnreads { + m1.MsgCount = o1.TotalMsgCount + m1.LastViewedAt = o1.LastPostAt + } + if withMentions { + m1.MentionCount = 5 + } + + m1, err = ss.Channel().UpdateMember(m1) + require.NoError(t, err) + + return *o1, *m1 + } + + type TestCase struct { + name string + pushProp string + userNotifyProp string + isDirect bool + withUnreads bool + withMentions bool + } + ttcc := []TestCase{} + + channelNotifyProps := []string{model.ChannelNotifyDefault, model.ChannelNotifyAll, model.ChannelNotifyMention, model.ChannelNotifyNone} + userNotifyProps := []string{model.UserNotifyAll, model.UserNotifyMention, model.UserNotifyHere, model.UserNotifyNone} + boolRange := []bool{true, false} + + nameTemplate := "pushProp: %s, userPushProp: %s, direct: %t, unreads: %t, mentions: %t" + for _, pushProp := range channelNotifyProps { + for _, userNotifyProp := range userNotifyProps { + for _, isDirect := range boolRange { + for _, withUnreads := range boolRange { + ttcc = append(ttcc, TestCase{ + name: fmt.Sprintf(nameTemplate, pushProp, userNotifyProp, isDirect, withUnreads, false), + pushProp: pushProp, + userNotifyProp: userNotifyProp, + isDirect: isDirect, + withUnreads: withUnreads, + withMentions: false, + }) + if withUnreads { + ttcc = append(ttcc, TestCase{ + name: fmt.Sprintf(nameTemplate, pushProp, userNotifyProp, isDirect, withUnreads, true), + pushProp: pushProp, + userNotifyProp: userNotifyProp, + isDirect: isDirect, + withUnreads: withUnreads, + withMentions: true, + }) + } + } + } + } + } + + for _, tc := range ttcc { + t.Run(tc.name, func(t *testing.T) { + o1, m1 := setupMembership(tc.pushProp, tc.withUnreads, tc.withMentions, tc.isDirect, model.NewId()) + userNotifyProps := model.GetDefaultChannelNotifyProps() + userNotifyProps[model.PushNotifyProp] = tc.userNotifyProp + unreads, mentions, times, err := ss.Channel().GetChannelsWithUnreadsAndWithMentions(context.Background(), []string{o1.Id}, m1.UserId, userNotifyProps) + require.NoError(t, err) + + expectedUnreadsLength := 0 + if tc.withUnreads { + expectedUnreadsLength = 1 + } + require.Len(t, unreads, expectedUnreadsLength) + + propToUse := tc.pushProp + if tc.pushProp == model.ChannelNotifyDefault { + propToUse = tc.userNotifyProp + } + expectedMentionsLength := 0 + if (tc.isDirect && tc.withUnreads) || (propToUse == model.UserNotifyAll && tc.withUnreads) || (propToUse == model.UserNotifyMention && tc.withMentions) { + expectedMentionsLength = 1 + } + + require.Len(t, mentions, expectedMentionsLength) + require.Equal(t, o1.LastPostAt, times[o1.Id]) + }) + } + + t.Run("multiple channels", func(t *testing.T) { + userId := model.NewId() + o1, _ := setupMembership(model.ChannelNotifyDefault, true, true, false, userId) + o2, _ := setupMembership(model.ChannelNotifyDefault, true, true, false, userId) + + userNotifyProps := model.GetDefaultChannelNotifyProps() + userNotifyProps[model.PushNotifyProp] = model.UserNotifyMention + + unreads, mentions, times, err := ss.Channel().GetChannelsWithUnreadsAndWithMentions(context.Background(), []string{o1.Id, o2.Id}, userId, userNotifyProps) + require.NoError(t, err) + + require.Contains(t, unreads, o1.Id) + require.Contains(t, unreads, o2.Id) + require.Contains(t, mentions, o1.Id) + require.Contains(t, mentions, o2.Id) + require.Equal(t, o1.LastPostAt, times[o1.Id]) + require.Equal(t, o2.LastPostAt, times[o2.Id]) + }) + + t.Run("non existing channel", func(t *testing.T) { + userNotifyProps := model.GetDefaultChannelNotifyProps() + userNotifyProps[model.PushNotifyProp] = model.UserNotifyMention + unreads, mentions, times, err := ss.Channel().GetChannelsWithUnreadsAndWithMentions(context.Background(), []string{"foo"}, "foo", userNotifyProps) + require.NoError(t, err) + + require.Len(t, unreads, 0) + require.Len(t, mentions, 0) + require.Len(t, times, 0) + }) +} diff --git a/server/channels/store/storetest/desktop_tokens_store.go b/server/channels/store/storetest/desktop_tokens_store.go new file mode 100644 index 00000000000..30a9ab1645f --- /dev/null +++ b/server/channels/store/storetest/desktop_tokens_store.go @@ -0,0 +1,110 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package storetest + +import ( + "testing" + + "github.com/mattermost/mattermost/server/v8/channels/store" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestDesktopTokensStore(t *testing.T, ss store.Store, s SqlStore) { + t.Run("GetUserId", func(t *testing.T) { testGetUserId(t, ss) }) + t.Run("Insert", func(t *testing.T) { testInsert(t, ss) }) + t.Run("Delete", func(t *testing.T) { testDeleteToken(t, ss) }) + t.Run("DeleteByUserId", func(t *testing.T) { testDeleteByUserId(t, ss) }) + t.Run("DeleteOlderThan", func(t *testing.T) { testDeleteOlderThan(t, ss) }) +} + +func testGetUserId(t *testing.T, ss store.Store) { + err := ss.DesktopTokens().Insert("token_with_id", 1000, "user_id") + require.NoError(t, err) + + t.Run("get user id", func(t *testing.T) { + userId, err := ss.DesktopTokens().GetUserId("token_with_id", 1000) + assert.NoError(t, err) + assert.Equal(t, "user_id", *userId) + }) + + t.Run("get user id - expired", func(t *testing.T) { + userId, err := ss.DesktopTokens().GetUserId("token_with_id", 10000) + assert.Error(t, err) + assert.IsType(t, &store.ErrNotFound{}, err) + assert.Nil(t, userId) + }) +} + +func testInsert(t *testing.T, ss store.Store) { + t.Run("insert", func(t *testing.T) { + err := ss.DesktopTokens().Insert("token", 1000, "user_id") + assert.NoError(t, err) + }) + + t.Run("insert - token too long", func(t *testing.T) { + err := ss.DesktopTokens().Insert( + "this token is way way way WAAAAAAAAAAAAAY WAAAAAAAAAAAAAY WAAAAAAAAAAAAAY TOO LONG", + 1000, + "user_id", + ) + assert.Error(t, err) + }) +} + +func testDeleteToken(t *testing.T, ss store.Store) { + err := ss.DesktopTokens().Insert("deleteable_token", 3000, "user_id") + require.NoError(t, err) + + t.Run("delete", func(t *testing.T) { + userId, err := ss.DesktopTokens().GetUserId("deleteable_token", 3000) + assert.NoError(t, err) + assert.Equal(t, "user_id", *userId) + + err = ss.DesktopTokens().Delete("deleteable_token") + assert.NoError(t, err) + + _, err = ss.DesktopTokens().GetUserId("deleteable_token", 3000) + assert.Error(t, err) + }) +} + +func testDeleteByUserId(t *testing.T, ss store.Store) { + err := ss.DesktopTokens().Insert("deleteable_token_2", 4000, "deleteable_user_id") + require.NoError(t, err) + + t.Run("delete by user id", func(t *testing.T) { + userId, err := ss.DesktopTokens().GetUserId("deleteable_token_2", 3000) + assert.NoError(t, err) + assert.Equal(t, "deleteable_user_id", *userId) + + err = ss.DesktopTokens().DeleteByUserId("deleteable_user_id") + assert.NoError(t, err) + + _, err = ss.DesktopTokens().GetUserId("deleteable_token_2", 3000) + assert.Error(t, err) + }) +} + +func testDeleteOlderThan(t *testing.T, ss store.Store) { + err := ss.DesktopTokens().Insert("deleteable_token_old", 1000, "deleteable_user_id") + require.NoError(t, err) + err = ss.DesktopTokens().Insert("deleteable_token_new", 5000, "deleteable_user_id") + require.NoError(t, err) + + t.Run("delete older than", func(t *testing.T) { + _, err := ss.DesktopTokens().GetUserId("deleteable_token_old", 1000) + assert.NoError(t, err) + _, err = ss.DesktopTokens().GetUserId("deleteable_token_new", 5000) + assert.NoError(t, err) + + err = ss.DesktopTokens().DeleteOlderThan(2000) + assert.NoError(t, err) + + _, err = ss.DesktopTokens().GetUserId("deleteable_token_old", 1000) + assert.Error(t, err) + _, err = ss.DesktopTokens().GetUserId("deleteable_token_new", 5000) + assert.NoError(t, err) + }) +} diff --git a/server/channels/store/storetest/emoji_store.go b/server/channels/store/storetest/emoji_store.go index bfb8ee9c0c3..189500994c8 100644 --- a/server/channels/store/storetest/emoji_store.go +++ b/server/channels/store/storetest/emoji_store.go @@ -4,11 +4,11 @@ package storetest import ( - "context" "testing" "time" "github.com/mattermost/mattermost/server/public/model" + "github.com/mattermost/mattermost/server/public/shared/request" "github.com/mattermost/mattermost/server/v8/channels/store" "github.com/stretchr/testify/assert" @@ -53,6 +53,8 @@ func testEmojiSaveDelete(t *testing.T, ss store.Store) { } func testEmojiGet(t *testing.T, ss store.Store) { + c := request.TestContext(t) + emojis := []model.Emoji{ { CreatorId: model.NewId(), @@ -81,17 +83,19 @@ func testEmojiGet(t *testing.T, ss store.Store) { }() for _, emoji := range emojis { - _, err := ss.Emoji().Get(context.Background(), emoji.Id, false) + _, err := ss.Emoji().Get(c, emoji.Id, false) require.NoErrorf(t, err, "failed to get emoji with id %v", emoji.Id) } for _, emoji := range emojis { - _, err := ss.Emoji().Get(context.Background(), emoji.Id, true) + _, err := ss.Emoji().Get(c, emoji.Id, true) require.NoErrorf(t, err, "failed to get emoji with id %v", emoji.Id) } } func testEmojiGetByName(t *testing.T, ss store.Store) { + c := request.TestContext(t) + emojis := []model.Emoji{ { CreatorId: model.NewId(), @@ -120,12 +124,14 @@ func testEmojiGetByName(t *testing.T, ss store.Store) { }() for _, emoji := range emojis { - _, err := ss.Emoji().GetByName(context.Background(), emoji.Name, true) + _, err := ss.Emoji().GetByName(c, emoji.Name, true) require.NoErrorf(t, err, "failed to get emoji with name %v", emoji.Name) } } func testEmojiGetMultipleByName(t *testing.T, ss store.Store) { + c := request.TestContext(t) + emojis := []model.Emoji{ { CreatorId: model.NewId(), @@ -154,26 +160,26 @@ func testEmojiGetMultipleByName(t *testing.T, ss store.Store) { }() t.Run("one emoji", func(t *testing.T) { - received, err := ss.Emoji().GetMultipleByName(context.Background(), []string{emojis[0].Name}) + received, err := ss.Emoji().GetMultipleByName(c, []string{emojis[0].Name}) require.NoError(t, err, "could not get emoji") require.Len(t, received, 1, "got incorrect emoji") require.Equal(t, *received[0], emojis[0], "got incorrect emoji") }) t.Run("multiple emojis", func(t *testing.T) { - received, err := ss.Emoji().GetMultipleByName(context.Background(), []string{emojis[0].Name, emojis[1].Name, emojis[2].Name}) + received, err := ss.Emoji().GetMultipleByName(c, []string{emojis[0].Name, emojis[1].Name, emojis[2].Name}) require.NoError(t, err, "could not get emojis") require.Len(t, received, 3, "got incorrect emojis") }) t.Run("one nonexistent emoji", func(t *testing.T) { - received, err := ss.Emoji().GetMultipleByName(context.Background(), []string{"ab"}) + received, err := ss.Emoji().GetMultipleByName(c, []string{"ab"}) require.NoError(t, err, "could not get emoji", err) require.Empty(t, received, "got incorrect emoji") }) t.Run("multiple emojis with nonexistent names", func(t *testing.T) { - received, err := ss.Emoji().GetMultipleByName(context.Background(), []string{emojis[0].Name, emojis[1].Name, emojis[2].Name, "abcd", "1234"}) + received, err := ss.Emoji().GetMultipleByName(c, []string{emojis[0].Name, emojis[1].Name, emojis[2].Name, "abcd", "1234"}) require.NoError(t, err, "could not get emojis") require.Len(t, received, 3, "got incorrect emojis") }) diff --git a/server/channels/store/storetest/group_store.go b/server/channels/store/storetest/group_store.go index a36ed750039..2e51beda2eb 100644 --- a/server/channels/store/storetest/group_store.go +++ b/server/channels/store/storetest/group_store.go @@ -16,8 +16,8 @@ import ( "github.com/stretchr/testify/require" "github.com/mattermost/mattermost/server/public/model" + pUtils "github.com/mattermost/mattermost/server/public/utils" "github.com/mattermost/mattermost/server/v8/channels/store" - "github.com/mattermost/mattermost/server/v8/channels/utils" ) func TestGroupStore(t *testing.T, ss store.Store) { @@ -3962,6 +3962,26 @@ func testGetGroups(t *testing.T, ss store.Store) { }, Restrictions: nil, }, + { + Name: "Include archived groups", + Opts: model.GroupSearchOpts{IncludeArchived: true, Q: "group-deleted"}, + Page: 0, + PerPage: 1, + Resultf: func(groups []*model.Group) bool { + return len(groups) == 1 + }, + Restrictions: nil, + }, + { + Name: "Only return archived groups", + Opts: model.GroupSearchOpts{FilterArchived: true, Q: "group-1"}, + Page: 0, + PerPage: 1, + Resultf: func(groups []*model.Group) bool { + return len(groups) == 0 + }, + Restrictions: nil, + }, } for _, tc := range testCases { @@ -4805,7 +4825,7 @@ func groupTestpUpdateMembersRoleTeam(t *testing.T, ss store.Store) { require.GreaterOrEqual(t, len(members), 4) // sanity check for team membership for _, member := range members { - if utils.StringInSlice(member.UserId, tt.inUserIDs) { + if pUtils.Contains(tt.inUserIDs, member.UserId) { require.True(t, member.SchemeAdmin) } else { require.False(t, member.SchemeAdmin) @@ -4915,7 +4935,7 @@ func groupTestpUpdateMembersRoleChannel(t *testing.T, ss store.Store) { require.GreaterOrEqual(t, len(members), 4) // sanity check for channel membership for _, member := range members { - if utils.StringInSlice(member.UserId, tt.inUserIDs) { + if pUtils.Contains(tt.inUserIDs, member.UserId) { require.True(t, member.SchemeAdmin) } else { require.False(t, member.SchemeAdmin) diff --git a/server/channels/store/storetest/job_store.go b/server/channels/store/storetest/job_store.go index 2f7557aca2a..52b8571da57 100644 --- a/server/channels/store/storetest/job_store.go +++ b/server/channels/store/storetest/job_store.go @@ -7,15 +7,15 @@ import ( "errors" "sync" "testing" - "time" "github.com/lib/pq" + "github.com/mattermost/mattermost/server/public/model" + "github.com/mattermost/mattermost/server/public/shared/mlog" + "github.com/mattermost/mattermost/server/public/shared/request" + "github.com/mattermost/mattermost/server/v8/channels/store" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - - "github.com/mattermost/mattermost/server/public/model" - "github.com/mattermost/mattermost/server/v8/channels/store" ) func TestJobStore(t *testing.T, ss store.Store) { @@ -25,7 +25,6 @@ func TestJobStore(t *testing.T, ss store.Store) { t.Run("JobGetAllByTypeAndStatus", func(t *testing.T) { testJobGetAllByTypeAndStatus(t, ss) }) t.Run("JobGetAllByTypePage", func(t *testing.T) { testJobGetAllByTypePage(t, ss) }) t.Run("JobGetAllByTypesPage", func(t *testing.T) { testJobGetAllByTypesPage(t, ss) }) - t.Run("JobGetAllPage", func(t *testing.T) { testJobGetAllPage(t, ss) }) t.Run("JobGetAllByStatus", func(t *testing.T) { testJobGetAllByStatus(t, ss) }) t.Run("GetNewestJobByStatusAndType", func(t *testing.T) { testJobStoreGetNewestJobByStatusAndType(t, ss) }) t.Run("GetNewestJobByStatusesAndType", func(t *testing.T) { testJobStoreGetNewestJobByStatusesAndType(t, ss) }) @@ -37,6 +36,8 @@ func TestJobStore(t *testing.T, ss store.Store) { } func testJobSaveGet(t *testing.T, ss store.Store) { + ctx := request.EmptyContext(mlog.CreateConsoleTestLogger(t)) + job := &model.Job{ Id: model.NewId(), Type: model.NewId(), @@ -53,7 +54,7 @@ func testJobSaveGet(t *testing.T, ss store.Store) { defer ss.Job().Delete(job.Id) - received, err := ss.Job().Get(job.Id) + received, err := ss.Job().Get(ctx, job.Id) require.NoError(t, err) require.Equal(t, job.Id, received.Id, "received incorrect job after save") require.Equal(t, "12345", received.Data["Total"]) @@ -105,6 +106,8 @@ func testJobSaveOnce(t *testing.T, ss store.Store) { } func testJobGetAllByType(t *testing.T, ss store.Store) { + ctx := request.EmptyContext(mlog.CreateConsoleTestLogger(t)) + jobType := model.NewId() jobs := []*model.Job{ @@ -128,13 +131,15 @@ func testJobGetAllByType(t *testing.T, ss store.Store) { defer ss.Job().Delete(job.Id) } - received, err := ss.Job().GetAllByType(jobType) + received, err := ss.Job().GetAllByType(ctx, jobType) require.NoError(t, err) require.Len(t, received, 2) require.ElementsMatch(t, []string{jobs[0].Id, jobs[1].Id}, []string{received[0].Id, received[1].Id}) } func testJobGetAllByTypeAndStatus(t *testing.T, ss store.Store) { + ctx := request.EmptyContext(mlog.CreateConsoleTestLogger(t)) + jobType := model.NewId() jobs := []*model.Job{ @@ -156,13 +161,15 @@ func testJobGetAllByTypeAndStatus(t *testing.T, ss store.Store) { defer ss.Job().Delete(job.Id) } - received, err := ss.Job().GetAllByTypeAndStatus(jobType, model.JobStatusPending) + received, err := ss.Job().GetAllByTypeAndStatus(ctx, jobType, model.JobStatusPending) require.NoError(t, err) require.Len(t, received, 2) require.ElementsMatch(t, []string{jobs[0].Id, jobs[1].Id}, []string{received[0].Id, received[1].Id}) } func testJobGetAllByTypePage(t *testing.T, ss store.Store) { + ctx := request.EmptyContext(mlog.CreateConsoleTestLogger(t)) + jobType := model.NewId() jobs := []*model.Job{ @@ -194,19 +201,21 @@ func testJobGetAllByTypePage(t *testing.T, ss store.Store) { defer ss.Job().Delete(job.Id) } - received, err := ss.Job().GetAllByTypePage(jobType, 0, 2) + received, err := ss.Job().GetAllByTypePage(ctx, jobType, 0, 2) require.NoError(t, err) require.Len(t, received, 2) require.Equal(t, received[0].Id, jobs[2].Id, "should've received newest job first") require.Equal(t, received[1].Id, jobs[0].Id, "should've received second newest job second") - received, err = ss.Job().GetAllByTypePage(jobType, 2, 2) + received, err = ss.Job().GetAllByTypePage(ctx, jobType, 2, 2) require.NoError(t, err) require.Len(t, received, 1) require.Equal(t, received[0].Id, jobs[1].Id, "should've received oldest job last") } func testJobGetAllByTypesPage(t *testing.T, ss store.Store) { + ctx := request.EmptyContext(mlog.CreateConsoleTestLogger(t)) + jobType := model.NewId() jobType2 := model.NewId() @@ -241,7 +250,7 @@ func testJobGetAllByTypesPage(t *testing.T, ss store.Store) { // test return all jobTypes := []string{jobType, jobType2} - received, err := ss.Job().GetAllByTypesPage(jobTypes, 0, 4) + received, err := ss.Job().GetAllByTypesPage(ctx, jobTypes, 0, 4) require.NoError(t, err) require.Len(t, received, 3) require.Equal(t, received[0].Id, jobs[2].Id, "should've received newest job first") @@ -249,59 +258,21 @@ func testJobGetAllByTypesPage(t *testing.T, ss store.Store) { // test paging jobTypes = []string{jobType, jobType2} - received, err = ss.Job().GetAllByTypesPage(jobTypes, 0, 2) + received, err = ss.Job().GetAllByTypesPage(ctx, jobTypes, 0, 2) require.NoError(t, err) require.Len(t, received, 2) require.Equal(t, received[0].Id, jobs[2].Id, "should've received newest job first") require.Equal(t, received[1].Id, jobs[0].Id, "should've received second newest job second") - received, err = ss.Job().GetAllByTypesPage(jobTypes, 2, 2) + received, err = ss.Job().GetAllByTypesPage(ctx, jobTypes, 2, 2) require.NoError(t, err) require.Len(t, received, 1) require.Equal(t, received[0].Id, jobs[1].Id, "should've received oldest job last") } -func testJobGetAllPage(t *testing.T, ss store.Store) { - jobType := model.NewId() - createAtTime := model.GetMillis() - - jobs := []*model.Job{ - { - Id: model.NewId(), - Type: jobType, - CreateAt: createAtTime + 1, - }, - { - Id: model.NewId(), - Type: jobType, - CreateAt: createAtTime, - }, - { - Id: model.NewId(), - Type: jobType, - CreateAt: createAtTime + 2, - }, - } - - for _, job := range jobs { - _, err := ss.Job().Save(job) - require.NoError(t, err) - defer ss.Job().Delete(job.Id) - } - - received, err := ss.Job().GetAllPage(0, 2) - require.NoError(t, err) - require.Len(t, received, 2) - require.Equal(t, received[0].Id, jobs[2].Id, "should've received newest job first") - require.Equal(t, received[1].Id, jobs[0].Id, "should've received second newest job second") - - received, err = ss.Job().GetAllPage(2, 2) - require.NoError(t, err) - require.NotEmpty(t, received) - require.Equal(t, received[0].Id, jobs[1].Id, "should've received oldest job last") -} - func testJobGetAllByStatus(t *testing.T, ss store.Store) { + ctx := request.EmptyContext(mlog.CreateConsoleTestLogger(t)) + jobType := model.NewId() status := model.NewId() @@ -341,7 +312,7 @@ func testJobGetAllByStatus(t *testing.T, ss store.Store) { defer ss.Job().Delete(job.Id) } - received, err := ss.Job().GetAllByStatus(status) + received, err := ss.Job().GetAllByStatus(ctx, status) require.NoError(t, err) require.Len(t, received, 3) require.Equal(t, received[0].Id, jobs[1].Id) @@ -521,6 +492,8 @@ func testJobStoreGetCountByStatusAndType(t *testing.T, ss store.Store) { } func testJobUpdateOptimistically(t *testing.T, ss store.Store) { + ctx := request.EmptyContext(mlog.CreateConsoleTestLogger(t)) + job := &model.Job{ Id: model.NewId(), Type: model.JobTypeDataRetention, @@ -548,7 +521,7 @@ func testJobUpdateOptimistically(t *testing.T, ss store.Store) { require.NoError(t, err) require.True(t, updated) - updatedJob, err := ss.Job().Get(job.Id) + updatedJob, err := ss.Job().Get(ctx, job.Id) require.NoError(t, err) require.Equal(t, updatedJob.Type, job.Type) @@ -560,6 +533,7 @@ func testJobUpdateOptimistically(t *testing.T, ss store.Store) { } func testJobUpdateStatusUpdateStatusOptimistically(t *testing.T, ss store.Store) { + ctx := request.EmptyContext(mlog.CreateConsoleTestLogger(t)) job := &model.Job{ Id: model.NewId(), Type: model.JobTypeDataRetention, @@ -589,7 +563,7 @@ func testJobUpdateStatusUpdateStatusOptimistically(t *testing.T, ss store.Store) require.NoError(t, err) require.False(t, updated) - received, err = ss.Job().Get(job.Id) + received, err = ss.Job().Get(ctx, job.Id) require.NoError(t, err) require.Equal(t, model.JobStatusPending, received.Status) @@ -602,7 +576,7 @@ func testJobUpdateStatusUpdateStatusOptimistically(t *testing.T, ss store.Store) require.True(t, updated, "should have succeeded") var startAtSet int64 - received, err = ss.Job().Get(job.Id) + received, err = ss.Job().Get(ctx, job.Id) require.NoError(t, err) require.Equal(t, model.JobStatusInProgress, received.Status) require.NotEqual(t, 0, received.StartAt) @@ -616,7 +590,7 @@ func testJobUpdateStatusUpdateStatusOptimistically(t *testing.T, ss store.Store) require.NoError(t, err) require.True(t, updated, "should have succeeded") - received, err = ss.Job().Get(job.Id) + received, err = ss.Job().Get(ctx, job.Id) require.NoError(t, err) require.Equal(t, model.JobStatusSuccess, received.Status) require.Equal(t, startAtSet, received.StartAt) @@ -632,6 +606,8 @@ func testJobDelete(t *testing.T, ss store.Store) { } func testJobCleanup(t *testing.T, ss store.Store) { + ctx := request.EmptyContext(mlog.CreateConsoleTestLogger(t)) + now := model.GetMillis() ids := make([]string, 0, 10) for i := 0; i < 10; i++ { @@ -645,7 +621,7 @@ func testJobCleanup(t *testing.T, ss store.Store) { defer ss.Job().Delete(job.Id) } - jobs, err := ss.Job().GetAllByStatus(model.JobStatusPending) + jobs, err := ss.Job().GetAllByStatus(ctx, model.JobStatusPending) require.NoError(t, err) assert.Len(t, jobs, 10) @@ -653,7 +629,7 @@ func testJobCleanup(t *testing.T, ss store.Store) { require.NoError(t, err) // Should not clean up pending jobs - jobs, err = ss.Job().GetAllByStatus(model.JobStatusPending) + jobs, err = ss.Job().GetAllByStatus(ctx, model.JobStatusPending) require.NoError(t, err) assert.Len(t, jobs, 10) @@ -666,7 +642,7 @@ func testJobCleanup(t *testing.T, ss store.Store) { require.NoError(t, err) // Should clean up now - jobs, err = ss.Job().GetAllByStatus(model.JobStatusSuccess) + jobs, err = ss.Job().GetAllByStatus(ctx, model.JobStatusSuccess) require.NoError(t, err) assert.Len(t, jobs, 0) } diff --git a/server/channels/store/storetest/license_store.go b/server/channels/store/storetest/license_store.go index 8705a0e127f..e167ab86342 100644 --- a/server/channels/store/storetest/license_store.go +++ b/server/channels/store/storetest/license_store.go @@ -4,6 +4,7 @@ package storetest import ( + "context" "testing" "github.com/stretchr/testify/require" @@ -22,15 +23,15 @@ func testLicenseStoreSave(t *testing.T, ss store.Store) { l1.Id = model.NewId() l1.Bytes = "junk" - _, err := ss.License().Save(&l1) + err := ss.License().Save(&l1) require.NoError(t, err, "couldn't save license record") - _, err = ss.License().Save(&l1) + err = ss.License().Save(&l1) require.NoError(t, err, "shouldn't fail on trying to save existing license record") l1.Id = "" - _, err = ss.License().Save(&l1) + err = ss.License().Save(&l1) require.Error(t, err, "should fail on invalid license") } @@ -39,14 +40,14 @@ func testLicenseStoreGet(t *testing.T, ss store.Store) { l1.Id = model.NewId() l1.Bytes = "junk" - _, err := ss.License().Save(&l1) + err := ss.License().Save(&l1) require.NoError(t, err) - record, err := ss.License().Get(l1.Id) + record, err := ss.License().Get(context.Background(), l1.Id) require.NoError(t, err, "couldn't get license") require.Equal(t, record.Bytes, l1.Bytes, "license bytes didn't match") - _, err = ss.License().Get("missing") + _, err = ss.License().Get(context.Background(), "missing") require.Error(t, err, "should fail on get license") } diff --git a/server/channels/store/storetest/mocks/ChannelStore.go b/server/channels/store/storetest/mocks/ChannelStore.go index 4d3500da1c8..9bf9f33f920 100644 --- a/server/channels/store/storetest/mocks/ChannelStore.go +++ b/server/channels/store/storetest/mocks/ChannelStore.go @@ -336,6 +336,20 @@ func (_m *ChannelStore) Delete(channelID string, timestamp int64) error { return r0 } +// DeleteAllSidebarChannelForChannel provides a mock function with given fields: channelID +func (_m *ChannelStore) DeleteAllSidebarChannelForChannel(channelID string) error { + ret := _m.Called(channelID) + + var r0 error + if rf, ok := ret.Get(0).(func(string) error); ok { + r0 = rf(channelID) + } else { + r0 = ret.Error(0) + } + + return r0 +} + // DeleteSidebarCategory provides a mock function with given fields: categoryID func (_m *ChannelStore) DeleteSidebarCategory(categoryID string) error { ret := _m.Called(categoryID) @@ -674,6 +688,32 @@ func (_m *ChannelStore) GetByNames(team_id string, names []string, allowFromCach return r0, r1 } +// GetByNamesIncludeDeleted provides a mock function with given fields: team_id, names, allowFromCache +func (_m *ChannelStore) GetByNamesIncludeDeleted(team_id string, names []string, allowFromCache bool) ([]*model.Channel, error) { + ret := _m.Called(team_id, names, allowFromCache) + + var r0 []*model.Channel + var r1 error + if rf, ok := ret.Get(0).(func(string, []string, bool) ([]*model.Channel, error)); ok { + return rf(team_id, names, allowFromCache) + } + if rf, ok := ret.Get(0).(func(string, []string, bool) []*model.Channel); ok { + r0 = rf(team_id, names, allowFromCache) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*model.Channel) + } + } + + if rf, ok := ret.Get(1).(func(string, []string, bool) error); ok { + r1 = rf(team_id, names, allowFromCache) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // GetChannelCounts provides a mock function with given fields: teamID, userID func (_m *ChannelStore) GetChannelCounts(teamID string, userID string) (*model.ChannelCounts, error) { ret := _m.Called(teamID, userID) @@ -700,25 +740,25 @@ func (_m *ChannelStore) GetChannelCounts(teamID string, userID string) (*model.C return r0, r1 } -// GetChannelMembersForExport provides a mock function with given fields: userID, teamID -func (_m *ChannelStore) GetChannelMembersForExport(userID string, teamID string) ([]*model.ChannelMemberForExport, error) { - ret := _m.Called(userID, teamID) +// GetChannelMembersForExport provides a mock function with given fields: userID, teamID, includeArchivedChannel +func (_m *ChannelStore) GetChannelMembersForExport(userID string, teamID string, includeArchivedChannel bool) ([]*model.ChannelMemberForExport, error) { + ret := _m.Called(userID, teamID, includeArchivedChannel) var r0 []*model.ChannelMemberForExport var r1 error - if rf, ok := ret.Get(0).(func(string, string) ([]*model.ChannelMemberForExport, error)); ok { - return rf(userID, teamID) + if rf, ok := ret.Get(0).(func(string, string, bool) ([]*model.ChannelMemberForExport, error)); ok { + return rf(userID, teamID, includeArchivedChannel) } - if rf, ok := ret.Get(0).(func(string, string) []*model.ChannelMemberForExport); ok { - r0 = rf(userID, teamID) + if rf, ok := ret.Get(0).(func(string, string, bool) []*model.ChannelMemberForExport); ok { + r0 = rf(userID, teamID, includeArchivedChannel) } else { if ret.Get(0) != nil { r0 = ret.Get(0).([]*model.ChannelMemberForExport) } } - if rf, ok := ret.Get(1).(func(string, string) error); ok { - r1 = rf(userID, teamID) + if rf, ok := ret.Get(1).(func(string, string, bool) error); ok { + r1 = rf(userID, teamID, includeArchivedChannel) } else { r1 = ret.Error(1) } @@ -986,6 +1026,50 @@ func (_m *ChannelStore) GetChannelsWithTeamDataByIds(channelIds []string, includ return r0, r1 } +// GetChannelsWithUnreadsAndWithMentions provides a mock function with given fields: ctx, channelIDs, userID, userNotifyProps +func (_m *ChannelStore) GetChannelsWithUnreadsAndWithMentions(ctx context.Context, channelIDs []string, userID string, userNotifyProps model.StringMap) ([]string, []string, map[string]int64, error) { + ret := _m.Called(ctx, channelIDs, userID, userNotifyProps) + + var r0 []string + var r1 []string + var r2 map[string]int64 + var r3 error + if rf, ok := ret.Get(0).(func(context.Context, []string, string, model.StringMap) ([]string, []string, map[string]int64, error)); ok { + return rf(ctx, channelIDs, userID, userNotifyProps) + } + if rf, ok := ret.Get(0).(func(context.Context, []string, string, model.StringMap) []string); ok { + r0 = rf(ctx, channelIDs, userID, userNotifyProps) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]string) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, []string, string, model.StringMap) []string); ok { + r1 = rf(ctx, channelIDs, userID, userNotifyProps) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).([]string) + } + } + + if rf, ok := ret.Get(2).(func(context.Context, []string, string, model.StringMap) map[string]int64); ok { + r2 = rf(ctx, channelIDs, userID, userNotifyProps) + } else { + if ret.Get(2) != nil { + r2 = ret.Get(2).(map[string]int64) + } + } + + if rf, ok := ret.Get(3).(func(context.Context, []string, string, model.StringMap) error); ok { + r3 = rf(ctx, channelIDs, userID, userNotifyProps) + } else { + r3 = ret.Error(3) + } + + return r0, r1, r2, r3 +} + // GetDeleted provides a mock function with given fields: team_id, offset, limit, userID func (_m *ChannelStore) GetDeleted(team_id string, offset int, limit int, userID string) (model.ChannelList, error) { ret := _m.Called(team_id, offset, limit, userID) diff --git a/server/channels/store/storetest/mocks/DesktopTokensStore.go b/server/channels/store/storetest/mocks/DesktopTokensStore.go new file mode 100644 index 00000000000..53412238be3 --- /dev/null +++ b/server/channels/store/storetest/mocks/DesktopTokensStore.go @@ -0,0 +1,109 @@ +// Code generated by mockery v2.23.2. DO NOT EDIT. + +// Regenerate this file using `make store-mocks`. + +package mocks + +import mock "github.com/stretchr/testify/mock" + +// DesktopTokensStore is an autogenerated mock type for the DesktopTokensStore type +type DesktopTokensStore struct { + mock.Mock +} + +// Delete provides a mock function with given fields: token +func (_m *DesktopTokensStore) Delete(token string) error { + ret := _m.Called(token) + + var r0 error + if rf, ok := ret.Get(0).(func(string) error); ok { + r0 = rf(token) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// DeleteByUserId provides a mock function with given fields: userId +func (_m *DesktopTokensStore) DeleteByUserId(userId string) error { + ret := _m.Called(userId) + + var r0 error + if rf, ok := ret.Get(0).(func(string) error); ok { + r0 = rf(userId) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// DeleteOlderThan provides a mock function with given fields: minCreatedAt +func (_m *DesktopTokensStore) DeleteOlderThan(minCreatedAt int64) error { + ret := _m.Called(minCreatedAt) + + var r0 error + if rf, ok := ret.Get(0).(func(int64) error); ok { + r0 = rf(minCreatedAt) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// GetUserId provides a mock function with given fields: token, minCreatedAt +func (_m *DesktopTokensStore) GetUserId(token string, minCreatedAt int64) (*string, error) { + ret := _m.Called(token, minCreatedAt) + + var r0 *string + var r1 error + if rf, ok := ret.Get(0).(func(string, int64) (*string, error)); ok { + return rf(token, minCreatedAt) + } + if rf, ok := ret.Get(0).(func(string, int64) *string); ok { + r0 = rf(token, minCreatedAt) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*string) + } + } + + if rf, ok := ret.Get(1).(func(string, int64) error); ok { + r1 = rf(token, minCreatedAt) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Insert provides a mock function with given fields: token, createAt, userId +func (_m *DesktopTokensStore) Insert(token string, createAt int64, userId string) error { + ret := _m.Called(token, createAt, userId) + + var r0 error + if rf, ok := ret.Get(0).(func(string, int64, string) error); ok { + r0 = rf(token, createAt, userId) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +type mockConstructorTestingTNewDesktopTokensStore interface { + mock.TestingT + Cleanup(func()) +} + +// NewDesktopTokensStore creates a new instance of DesktopTokensStore. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +func NewDesktopTokensStore(t mockConstructorTestingTNewDesktopTokensStore) *DesktopTokensStore { + mock := &DesktopTokensStore{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/server/channels/store/storetest/mocks/EmojiStore.go b/server/channels/store/storetest/mocks/EmojiStore.go index 9c1045a8e3c..58e25fd8382 100644 --- a/server/channels/store/storetest/mocks/EmojiStore.go +++ b/server/channels/store/storetest/mocks/EmojiStore.go @@ -5,9 +5,8 @@ package mocks import ( - context "context" - model "github.com/mattermost/mattermost/server/public/model" + request "github.com/mattermost/mattermost/server/public/shared/request" mock "github.com/stretchr/testify/mock" ) @@ -31,15 +30,15 @@ func (_m *EmojiStore) Delete(emoji *model.Emoji, timestamp int64) error { } // Get provides a mock function with given fields: ctx, id, allowFromCache -func (_m *EmojiStore) Get(ctx context.Context, id string, allowFromCache bool) (*model.Emoji, error) { +func (_m *EmojiStore) Get(ctx request.CTX, id string, allowFromCache bool) (*model.Emoji, error) { ret := _m.Called(ctx, id, allowFromCache) var r0 *model.Emoji var r1 error - if rf, ok := ret.Get(0).(func(context.Context, string, bool) (*model.Emoji, error)); ok { + if rf, ok := ret.Get(0).(func(request.CTX, string, bool) (*model.Emoji, error)); ok { return rf(ctx, id, allowFromCache) } - if rf, ok := ret.Get(0).(func(context.Context, string, bool) *model.Emoji); ok { + if rf, ok := ret.Get(0).(func(request.CTX, string, bool) *model.Emoji); ok { r0 = rf(ctx, id, allowFromCache) } else { if ret.Get(0) != nil { @@ -47,7 +46,7 @@ func (_m *EmojiStore) Get(ctx context.Context, id string, allowFromCache bool) ( } } - if rf, ok := ret.Get(1).(func(context.Context, string, bool) error); ok { + if rf, ok := ret.Get(1).(func(request.CTX, string, bool) error); ok { r1 = rf(ctx, id, allowFromCache) } else { r1 = ret.Error(1) @@ -57,15 +56,15 @@ func (_m *EmojiStore) Get(ctx context.Context, id string, allowFromCache bool) ( } // GetByName provides a mock function with given fields: ctx, name, allowFromCache -func (_m *EmojiStore) GetByName(ctx context.Context, name string, allowFromCache bool) (*model.Emoji, error) { +func (_m *EmojiStore) GetByName(ctx request.CTX, name string, allowFromCache bool) (*model.Emoji, error) { ret := _m.Called(ctx, name, allowFromCache) var r0 *model.Emoji var r1 error - if rf, ok := ret.Get(0).(func(context.Context, string, bool) (*model.Emoji, error)); ok { + if rf, ok := ret.Get(0).(func(request.CTX, string, bool) (*model.Emoji, error)); ok { return rf(ctx, name, allowFromCache) } - if rf, ok := ret.Get(0).(func(context.Context, string, bool) *model.Emoji); ok { + if rf, ok := ret.Get(0).(func(request.CTX, string, bool) *model.Emoji); ok { r0 = rf(ctx, name, allowFromCache) } else { if ret.Get(0) != nil { @@ -73,7 +72,7 @@ func (_m *EmojiStore) GetByName(ctx context.Context, name string, allowFromCache } } - if rf, ok := ret.Get(1).(func(context.Context, string, bool) error); ok { + if rf, ok := ret.Get(1).(func(request.CTX, string, bool) error); ok { r1 = rf(ctx, name, allowFromCache) } else { r1 = ret.Error(1) @@ -109,15 +108,15 @@ func (_m *EmojiStore) GetList(offset int, limit int, sort string) ([]*model.Emoj } // GetMultipleByName provides a mock function with given fields: ctx, names -func (_m *EmojiStore) GetMultipleByName(ctx context.Context, names []string) ([]*model.Emoji, error) { +func (_m *EmojiStore) GetMultipleByName(ctx request.CTX, names []string) ([]*model.Emoji, error) { ret := _m.Called(ctx, names) var r0 []*model.Emoji var r1 error - if rf, ok := ret.Get(0).(func(context.Context, []string) ([]*model.Emoji, error)); ok { + if rf, ok := ret.Get(0).(func(request.CTX, []string) ([]*model.Emoji, error)); ok { return rf(ctx, names) } - if rf, ok := ret.Get(0).(func(context.Context, []string) []*model.Emoji); ok { + if rf, ok := ret.Get(0).(func(request.CTX, []string) []*model.Emoji); ok { r0 = rf(ctx, names) } else { if ret.Get(0) != nil { @@ -125,7 +124,7 @@ func (_m *EmojiStore) GetMultipleByName(ctx context.Context, names []string) ([] } } - if rf, ok := ret.Get(1).(func(context.Context, []string) error); ok { + if rf, ok := ret.Get(1).(func(request.CTX, []string) error); ok { r1 = rf(ctx, names) } else { r1 = ret.Error(1) diff --git a/server/channels/store/storetest/mocks/JobStore.go b/server/channels/store/storetest/mocks/JobStore.go index c008f343399..bd3e3e17450 100644 --- a/server/channels/store/storetest/mocks/JobStore.go +++ b/server/channels/store/storetest/mocks/JobStore.go @@ -6,6 +6,7 @@ package mocks import ( model "github.com/mattermost/mattermost/server/public/model" + request "github.com/mattermost/mattermost/server/public/shared/request" mock "github.com/stretchr/testify/mock" ) @@ -52,25 +53,25 @@ func (_m *JobStore) Delete(id string) (string, error) { return r0, r1 } -// Get provides a mock function with given fields: id -func (_m *JobStore) Get(id string) (*model.Job, error) { - ret := _m.Called(id) +// Get provides a mock function with given fields: c, id +func (_m *JobStore) Get(c *request.Context, id string) (*model.Job, error) { + ret := _m.Called(c, id) var r0 *model.Job var r1 error - if rf, ok := ret.Get(0).(func(string) (*model.Job, error)); ok { - return rf(id) + if rf, ok := ret.Get(0).(func(*request.Context, string) (*model.Job, error)); ok { + return rf(c, id) } - if rf, ok := ret.Get(0).(func(string) *model.Job); ok { - r0 = rf(id) + if rf, ok := ret.Get(0).(func(*request.Context, string) *model.Job); ok { + r0 = rf(c, id) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(*model.Job) } } - if rf, ok := ret.Get(1).(func(string) error); ok { - r1 = rf(id) + if rf, ok := ret.Get(1).(func(*request.Context, string) error); ok { + r1 = rf(c, id) } else { r1 = ret.Error(1) } @@ -78,25 +79,25 @@ func (_m *JobStore) Get(id string) (*model.Job, error) { return r0, r1 } -// GetAllByStatus provides a mock function with given fields: status -func (_m *JobStore) GetAllByStatus(status string) ([]*model.Job, error) { - ret := _m.Called(status) +// GetAllByStatus provides a mock function with given fields: c, status +func (_m *JobStore) GetAllByStatus(c *request.Context, status string) ([]*model.Job, error) { + ret := _m.Called(c, status) var r0 []*model.Job var r1 error - if rf, ok := ret.Get(0).(func(string) ([]*model.Job, error)); ok { - return rf(status) + if rf, ok := ret.Get(0).(func(*request.Context, string) ([]*model.Job, error)); ok { + return rf(c, status) } - if rf, ok := ret.Get(0).(func(string) []*model.Job); ok { - r0 = rf(status) + if rf, ok := ret.Get(0).(func(*request.Context, string) []*model.Job); ok { + r0 = rf(c, status) } else { if ret.Get(0) != nil { r0 = ret.Get(0).([]*model.Job) } } - if rf, ok := ret.Get(1).(func(string) error); ok { - r1 = rf(status) + if rf, ok := ret.Get(1).(func(*request.Context, string) error); ok { + r1 = rf(c, status) } else { r1 = ret.Error(1) } @@ -104,25 +105,25 @@ func (_m *JobStore) GetAllByStatus(status string) ([]*model.Job, error) { return r0, r1 } -// GetAllByType provides a mock function with given fields: jobType -func (_m *JobStore) GetAllByType(jobType string) ([]*model.Job, error) { - ret := _m.Called(jobType) +// GetAllByType provides a mock function with given fields: c, jobType +func (_m *JobStore) GetAllByType(c *request.Context, jobType string) ([]*model.Job, error) { + ret := _m.Called(c, jobType) var r0 []*model.Job var r1 error - if rf, ok := ret.Get(0).(func(string) ([]*model.Job, error)); ok { - return rf(jobType) + if rf, ok := ret.Get(0).(func(*request.Context, string) ([]*model.Job, error)); ok { + return rf(c, jobType) } - if rf, ok := ret.Get(0).(func(string) []*model.Job); ok { - r0 = rf(jobType) + if rf, ok := ret.Get(0).(func(*request.Context, string) []*model.Job); ok { + r0 = rf(c, jobType) } else { if ret.Get(0) != nil { r0 = ret.Get(0).([]*model.Job) } } - if rf, ok := ret.Get(1).(func(string) error); ok { - r1 = rf(jobType) + if rf, ok := ret.Get(1).(func(*request.Context, string) error); ok { + r1 = rf(c, jobType) } else { r1 = ret.Error(1) } @@ -130,25 +131,25 @@ func (_m *JobStore) GetAllByType(jobType string) ([]*model.Job, error) { return r0, r1 } -// GetAllByTypeAndStatus provides a mock function with given fields: jobType, status -func (_m *JobStore) GetAllByTypeAndStatus(jobType string, status string) ([]*model.Job, error) { - ret := _m.Called(jobType, status) +// GetAllByTypeAndStatus provides a mock function with given fields: c, jobType, status +func (_m *JobStore) GetAllByTypeAndStatus(c *request.Context, jobType string, status string) ([]*model.Job, error) { + ret := _m.Called(c, jobType, status) var r0 []*model.Job var r1 error - if rf, ok := ret.Get(0).(func(string, string) ([]*model.Job, error)); ok { - return rf(jobType, status) + if rf, ok := ret.Get(0).(func(*request.Context, string, string) ([]*model.Job, error)); ok { + return rf(c, jobType, status) } - if rf, ok := ret.Get(0).(func(string, string) []*model.Job); ok { - r0 = rf(jobType, status) + if rf, ok := ret.Get(0).(func(*request.Context, string, string) []*model.Job); ok { + r0 = rf(c, jobType, status) } else { if ret.Get(0) != nil { r0 = ret.Get(0).([]*model.Job) } } - if rf, ok := ret.Get(1).(func(string, string) error); ok { - r1 = rf(jobType, status) + if rf, ok := ret.Get(1).(func(*request.Context, string, string) error); ok { + r1 = rf(c, jobType, status) } else { r1 = ret.Error(1) } @@ -156,25 +157,25 @@ func (_m *JobStore) GetAllByTypeAndStatus(jobType string, status string) ([]*mod return r0, r1 } -// GetAllByTypePage provides a mock function with given fields: jobType, offset, limit -func (_m *JobStore) GetAllByTypePage(jobType string, offset int, limit int) ([]*model.Job, error) { - ret := _m.Called(jobType, offset, limit) +// GetAllByTypePage provides a mock function with given fields: c, jobType, offset, limit +func (_m *JobStore) GetAllByTypePage(c *request.Context, jobType string, offset int, limit int) ([]*model.Job, error) { + ret := _m.Called(c, jobType, offset, limit) var r0 []*model.Job var r1 error - if rf, ok := ret.Get(0).(func(string, int, int) ([]*model.Job, error)); ok { - return rf(jobType, offset, limit) + if rf, ok := ret.Get(0).(func(*request.Context, string, int, int) ([]*model.Job, error)); ok { + return rf(c, jobType, offset, limit) } - if rf, ok := ret.Get(0).(func(string, int, int) []*model.Job); ok { - r0 = rf(jobType, offset, limit) + if rf, ok := ret.Get(0).(func(*request.Context, string, int, int) []*model.Job); ok { + r0 = rf(c, jobType, offset, limit) } else { if ret.Get(0) != nil { r0 = ret.Get(0).([]*model.Job) } } - if rf, ok := ret.Get(1).(func(string, int, int) error); ok { - r1 = rf(jobType, offset, limit) + if rf, ok := ret.Get(1).(func(*request.Context, string, int, int) error); ok { + r1 = rf(c, jobType, offset, limit) } else { r1 = ret.Error(1) } @@ -182,51 +183,25 @@ func (_m *JobStore) GetAllByTypePage(jobType string, offset int, limit int) ([]* return r0, r1 } -// GetAllByTypesPage provides a mock function with given fields: jobTypes, offset, limit -func (_m *JobStore) GetAllByTypesPage(jobTypes []string, offset int, limit int) ([]*model.Job, error) { - ret := _m.Called(jobTypes, offset, limit) +// GetAllByTypesPage provides a mock function with given fields: c, jobTypes, offset, limit +func (_m *JobStore) GetAllByTypesPage(c *request.Context, jobTypes []string, offset int, limit int) ([]*model.Job, error) { + ret := _m.Called(c, jobTypes, offset, limit) var r0 []*model.Job var r1 error - if rf, ok := ret.Get(0).(func([]string, int, int) ([]*model.Job, error)); ok { - return rf(jobTypes, offset, limit) + if rf, ok := ret.Get(0).(func(*request.Context, []string, int, int) ([]*model.Job, error)); ok { + return rf(c, jobTypes, offset, limit) } - if rf, ok := ret.Get(0).(func([]string, int, int) []*model.Job); ok { - r0 = rf(jobTypes, offset, limit) + if rf, ok := ret.Get(0).(func(*request.Context, []string, int, int) []*model.Job); ok { + r0 = rf(c, jobTypes, offset, limit) } else { if ret.Get(0) != nil { r0 = ret.Get(0).([]*model.Job) } } - if rf, ok := ret.Get(1).(func([]string, int, int) error); ok { - r1 = rf(jobTypes, offset, limit) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// GetAllPage provides a mock function with given fields: offset, limit -func (_m *JobStore) GetAllPage(offset int, limit int) ([]*model.Job, error) { - ret := _m.Called(offset, limit) - - var r0 []*model.Job - var r1 error - if rf, ok := ret.Get(0).(func(int, int) ([]*model.Job, error)); ok { - return rf(offset, limit) - } - if rf, ok := ret.Get(0).(func(int, int) []*model.Job); ok { - r0 = rf(offset, limit) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).([]*model.Job) - } - } - - if rf, ok := ret.Get(1).(func(int, int) error); ok { - r1 = rf(offset, limit) + if rf, ok := ret.Get(1).(func(*request.Context, []string, int, int) error); ok { + r1 = rf(c, jobTypes, offset, limit) } else { r1 = ret.Error(1) } diff --git a/server/channels/store/storetest/mocks/LicenseStore.go b/server/channels/store/storetest/mocks/LicenseStore.go index 155c2de9679..04c9ddd764f 100644 --- a/server/channels/store/storetest/mocks/LicenseStore.go +++ b/server/channels/store/storetest/mocks/LicenseStore.go @@ -5,6 +5,8 @@ package mocks import ( + context "context" + model "github.com/mattermost/mattermost/server/public/model" mock "github.com/stretchr/testify/mock" ) @@ -14,25 +16,25 @@ type LicenseStore struct { mock.Mock } -// Get provides a mock function with given fields: id -func (_m *LicenseStore) Get(id string) (*model.LicenseRecord, error) { - ret := _m.Called(id) +// Get provides a mock function with given fields: ctx, id +func (_m *LicenseStore) Get(ctx context.Context, id string) (*model.LicenseRecord, error) { + ret := _m.Called(ctx, id) var r0 *model.LicenseRecord var r1 error - if rf, ok := ret.Get(0).(func(string) (*model.LicenseRecord, error)); ok { - return rf(id) + if rf, ok := ret.Get(0).(func(context.Context, string) (*model.LicenseRecord, error)); ok { + return rf(ctx, id) } - if rf, ok := ret.Get(0).(func(string) *model.LicenseRecord); ok { - r0 = rf(id) + if rf, ok := ret.Get(0).(func(context.Context, string) *model.LicenseRecord); ok { + r0 = rf(ctx, id) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(*model.LicenseRecord) } } - if rf, ok := ret.Get(1).(func(string) error); ok { - r1 = rf(id) + if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { + r1 = rf(ctx, id) } else { r1 = ret.Error(1) } @@ -67,29 +69,17 @@ func (_m *LicenseStore) GetAll() ([]*model.LicenseRecord, error) { } // Save provides a mock function with given fields: license -func (_m *LicenseStore) Save(license *model.LicenseRecord) (*model.LicenseRecord, error) { +func (_m *LicenseStore) Save(license *model.LicenseRecord) error { ret := _m.Called(license) - var r0 *model.LicenseRecord - var r1 error - if rf, ok := ret.Get(0).(func(*model.LicenseRecord) (*model.LicenseRecord, error)); ok { - return rf(license) - } - if rf, ok := ret.Get(0).(func(*model.LicenseRecord) *model.LicenseRecord); ok { + var r0 error + if rf, ok := ret.Get(0).(func(*model.LicenseRecord) error); ok { r0 = rf(license) } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*model.LicenseRecord) - } + r0 = ret.Error(0) } - if rf, ok := ret.Get(1).(func(*model.LicenseRecord) error); ok { - r1 = rf(license) - } else { - r1 = ret.Error(1) - } - - return r0, r1 + return r0 } type mockConstructorTestingTNewLicenseStore interface { diff --git a/server/channels/store/storetest/mocks/PostStore.go b/server/channels/store/storetest/mocks/PostStore.go index 92e7c56db8c..cd2fe223525 100644 --- a/server/channels/store/storetest/mocks/PostStore.go +++ b/server/channels/store/storetest/mocks/PostStore.go @@ -113,30 +113,6 @@ func (_m *PostStore) Delete(postID string, timestamp int64, deleteByID string) e return r0 } -// DeleteOrphanedRows provides a mock function with given fields: limit -func (_m *PostStore) DeleteOrphanedRows(limit int) (int64, error) { - ret := _m.Called(limit) - - var r0 int64 - var r1 error - if rf, ok := ret.Get(0).(func(int) (int64, error)); ok { - return rf(limit) - } - if rf, ok := ret.Get(0).(func(int) int64); ok { - r0 = rf(limit) - } else { - r0 = ret.Get(0).(int64) - } - - if rf, ok := ret.Get(1).(func(int) error); ok { - r1 = rf(limit) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - // Get provides a mock function with given fields: ctx, id, opts, userID, sanitizeOptions func (_m *PostStore) Get(ctx context.Context, id string, opts model.GetPostsOptions, userID string, sanitizeOptions map[string]bool) (*model.PostList, error) { ret := _m.Called(ctx, id, opts, userID, sanitizeOptions) @@ -395,25 +371,25 @@ func (_m *PostStore) GetOldestEntityCreationTime() (int64, error) { return r0, r1 } -// GetParentsForExportAfter provides a mock function with given fields: limit, afterID -func (_m *PostStore) GetParentsForExportAfter(limit int, afterID string) ([]*model.PostForExport, error) { - ret := _m.Called(limit, afterID) +// GetParentsForExportAfter provides a mock function with given fields: limit, afterID, includeArchivedChannels +func (_m *PostStore) GetParentsForExportAfter(limit int, afterID string, includeArchivedChannels bool) ([]*model.PostForExport, error) { + ret := _m.Called(limit, afterID, includeArchivedChannels) var r0 []*model.PostForExport var r1 error - if rf, ok := ret.Get(0).(func(int, string) ([]*model.PostForExport, error)); ok { - return rf(limit, afterID) + if rf, ok := ret.Get(0).(func(int, string, bool) ([]*model.PostForExport, error)); ok { + return rf(limit, afterID, includeArchivedChannels) } - if rf, ok := ret.Get(0).(func(int, string) []*model.PostForExport); ok { - r0 = rf(limit, afterID) + if rf, ok := ret.Get(0).(func(int, string, bool) []*model.PostForExport); ok { + r0 = rf(limit, afterID, includeArchivedChannels) } else { if ret.Get(0) != nil { r0 = ret.Get(0).([]*model.PostForExport) } } - if rf, ok := ret.Get(1).(func(int, string) error); ok { - r1 = rf(limit, afterID) + if rf, ok := ret.Get(1).(func(int, string, bool) error); ok { + r1 = rf(limit, afterID, includeArchivedChannels) } else { r1 = ret.Error(1) } diff --git a/server/channels/store/storetest/mocks/ReactionStore.go b/server/channels/store/storetest/mocks/ReactionStore.go index 2ca3f86e0db..966010f6689 100644 --- a/server/channels/store/storetest/mocks/ReactionStore.go +++ b/server/channels/store/storetest/mocks/ReactionStore.go @@ -80,28 +80,18 @@ func (_m *ReactionStore) DeleteAllWithEmojiName(emojiName string) error { return r0 } -// DeleteOrphanedRows provides a mock function with given fields: limit -func (_m *ReactionStore) DeleteOrphanedRows(limit int) (int64, error) { - ret := _m.Called(limit) +// DeleteOrphanedRowsByIds provides a mock function with given fields: r +func (_m *ReactionStore) DeleteOrphanedRowsByIds(r *model.RetentionIdsForDeletion) error { + ret := _m.Called(r) - var r0 int64 - var r1 error - if rf, ok := ret.Get(0).(func(int) (int64, error)); ok { - return rf(limit) - } - if rf, ok := ret.Get(0).(func(int) int64); ok { - r0 = rf(limit) + var r0 error + if rf, ok := ret.Get(0).(func(*model.RetentionIdsForDeletion) error); ok { + r0 = rf(r) } else { - r0 = ret.Get(0).(int64) + r0 = ret.Error(0) } - if rf, ok := ret.Get(1).(func(int) error); ok { - r1 = rf(limit) - } else { - r1 = ret.Error(1) - } - - return r0, r1 + return r0 } // GetForPost provides a mock function with given fields: postID, allowFromCache @@ -180,6 +170,20 @@ func (_m *ReactionStore) PermanentDeleteBatch(endTime int64, limit int64) (int64 return r0, r1 } +// PermanentDeleteByUser provides a mock function with given fields: userID +func (_m *ReactionStore) PermanentDeleteByUser(userID string) error { + ret := _m.Called(userID) + + var r0 error + if rf, ok := ret.Get(0).(func(string) error); ok { + r0 = rf(userID) + } else { + r0 = ret.Error(0) + } + + return r0 +} + // Save provides a mock function with given fields: reaction func (_m *ReactionStore) Save(reaction *model.Reaction) (*model.Reaction, error) { ret := _m.Called(reaction) diff --git a/server/channels/store/storetest/mocks/RetentionPolicyStore.go b/server/channels/store/storetest/mocks/RetentionPolicyStore.go index 73b7ef791e4..fa20e4b5c9a 100644 --- a/server/channels/store/storetest/mocks/RetentionPolicyStore.go +++ b/server/channels/store/storetest/mocks/RetentionPolicyStore.go @@ -256,6 +256,32 @@ func (_m *RetentionPolicyStore) GetCount() (int64, error) { return r0, r1 } +// GetIdsForDeletionByTableName provides a mock function with given fields: tableName, limit +func (_m *RetentionPolicyStore) GetIdsForDeletionByTableName(tableName string, limit int) ([]*model.RetentionIdsForDeletion, error) { + ret := _m.Called(tableName, limit) + + var r0 []*model.RetentionIdsForDeletion + var r1 error + if rf, ok := ret.Get(0).(func(string, int) ([]*model.RetentionIdsForDeletion, error)); ok { + return rf(tableName, limit) + } + if rf, ok := ret.Get(0).(func(string, int) []*model.RetentionIdsForDeletion); ok { + r0 = rf(tableName, limit) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*model.RetentionIdsForDeletion) + } + } + + if rf, ok := ret.Get(1).(func(string, int) error); ok { + r1 = rf(tableName, limit) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // GetTeamPoliciesCountForUser provides a mock function with given fields: userID func (_m *RetentionPolicyStore) GetTeamPoliciesCountForUser(userID string) (int64, error) { ret := _m.Called(userID) diff --git a/server/channels/store/storetest/mocks/Store.go b/server/channels/store/storetest/mocks/Store.go index bc8f8c30315..da01a1e258b 100644 --- a/server/channels/store/storetest/mocks/Store.go +++ b/server/channels/store/storetest/mocks/Store.go @@ -187,6 +187,22 @@ func (_m *Store) Context() context.Context { return r0 } +// DesktopTokens provides a mock function with given fields: +func (_m *Store) DesktopTokens() store.DesktopTokensStore { + ret := _m.Called() + + var r0 store.DesktopTokensStore + if rf, ok := ret.Get(0).(func() store.DesktopTokensStore); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(store.DesktopTokensStore) + } + } + + return r0 +} + // Draft provides a mock function with given fields: func (_m *Store) Draft() store.DraftStore { ret := _m.Called() diff --git a/server/channels/store/storetest/mocks/TeamStore.go b/server/channels/store/storetest/mocks/TeamStore.go index 9ac7094b165..110886c2f97 100644 --- a/server/channels/store/storetest/mocks/TeamStore.go +++ b/server/channels/store/storetest/mocks/TeamStore.go @@ -419,6 +419,32 @@ func (_m *TeamStore) GetChannelUnreadsForTeam(teamID string, userID string) ([]* return r0, r1 } +// GetCommonTeamIDsForMultipleUsers provides a mock function with given fields: userIDs +func (_m *TeamStore) GetCommonTeamIDsForMultipleUsers(userIDs []string) ([]string, error) { + ret := _m.Called(userIDs) + + var r0 []string + var r1 error + if rf, ok := ret.Get(0).(func([]string) ([]string, error)); ok { + return rf(userIDs) + } + if rf, ok := ret.Get(0).(func([]string) []string); ok { + r0 = rf(userIDs) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]string) + } + } + + if rf, ok := ret.Get(1).(func([]string) error); ok { + r1 = rf(userIDs) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // GetCommonTeamIDsForTwoUsers provides a mock function with given fields: userID, otherUserID func (_m *TeamStore) GetCommonTeamIDsForTwoUsers(userID string, otherUserID string) ([]string, error) { ret := _m.Called(userID, otherUserID) diff --git a/server/channels/store/storetest/mocks/UserStore.go b/server/channels/store/storetest/mocks/UserStore.go index a0f7c11f6d1..48ef53822d6 100644 --- a/server/channels/store/storetest/mocks/UserStore.go +++ b/server/channels/store/storetest/mocks/UserStore.go @@ -541,6 +541,32 @@ func (_m *UserStore) GetByEmail(email string) (*model.User, error) { return r0, r1 } +// GetByRemoteID provides a mock function with given fields: remoteID +func (_m *UserStore) GetByRemoteID(remoteID string) (*model.User, error) { + ret := _m.Called(remoteID) + + var r0 *model.User + var r1 error + if rf, ok := ret.Get(0).(func(string) (*model.User, error)); ok { + return rf(remoteID) + } + if rf, ok := ret.Get(0).(func(string) *model.User); ok { + r0 = rf(remoteID) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*model.User) + } + } + + if rf, ok := ret.Get(1).(func(string) error); ok { + r1 = rf(remoteID) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // GetByUsername provides a mock function with given fields: username func (_m *UserStore) GetByUsername(username string) (*model.User, error) { ret := _m.Called(username) diff --git a/server/channels/store/storetest/post_store.go b/server/channels/store/storetest/post_store.go index 5e0324189a9..c52083b71e5 100644 --- a/server/channels/store/storetest/post_store.go +++ b/server/channels/store/storetest/post_store.go @@ -744,7 +744,6 @@ func testPostStoreGetForThread(t *testing.T, ss store.Store) { }) t.Run("Pagination", func(t *testing.T) { - t.Skip("MM-46134") teamId := model.NewId() channel, err := ss.Channel().Save(&model.Channel{ TeamId: teamId, @@ -754,15 +753,16 @@ func testPostStoreGetForThread(t *testing.T, ss store.Store) { }, -1) require.NoError(t, err) - o1, err := ss.Post().Save(&model.Post{ChannelId: channel.Id, UserId: model.NewId(), Message: NewTestId()}) + now := model.GetMillis() + o1, err := ss.Post().Save(&model.Post{CreateAt: now, ChannelId: channel.Id, UserId: model.NewId(), Message: NewTestId()}) require.NoError(t, err) - _, err = ss.Post().Save(&model.Post{ChannelId: o1.ChannelId, UserId: model.NewId(), Message: NewTestId(), RootId: o1.Id}) + _, err = ss.Post().Save(&model.Post{CreateAt: now + 1, ChannelId: o1.ChannelId, UserId: model.NewId(), Message: NewTestId(), RootId: o1.Id}) require.NoError(t, err) - m1, err := ss.Post().Save(&model.Post{ChannelId: o1.ChannelId, UserId: model.NewId(), Message: NewTestId(), RootId: o1.Id}) + m1, err := ss.Post().Save(&model.Post{CreateAt: now + 2, ChannelId: o1.ChannelId, UserId: model.NewId(), Message: NewTestId(), RootId: o1.Id}) require.NoError(t, err) - _, err = ss.Post().Save(&model.Post{ChannelId: o1.ChannelId, UserId: model.NewId(), Message: NewTestId(), RootId: o1.Id}) + _, err = ss.Post().Save(&model.Post{CreateAt: now + 3, ChannelId: o1.ChannelId, UserId: model.NewId(), Message: NewTestId(), RootId: o1.Id}) require.NoError(t, err) - _, err = ss.Post().Save(&model.Post{ChannelId: o1.ChannelId, UserId: model.NewId(), Message: NewTestId(), RootId: o1.Id}) + _, err = ss.Post().Save(&model.Post{CreateAt: now + 4, ChannelId: o1.ChannelId, UserId: model.NewId(), Message: NewTestId(), RootId: o1.Id}) require.NoError(t, err) opts := model.GetPostsOptions{ @@ -772,7 +772,8 @@ func testPostStoreGetForThread(t *testing.T, ss store.Store) { } r1, err := ss.Post().Get(context.Background(), o1.Id, opts, o1.UserId, map[string]bool{}) require.NoError(t, err) - assert.Len(t, r1.Order, 3) // including the root post + require.Len(t, r1.Order, 3) // including the root post + require.Len(t, r1.Posts, 3) assert.True(t, r1.HasNext) lastPostID := r1.Order[len(r1.Order)-1] @@ -787,7 +788,8 @@ func testPostStoreGetForThread(t *testing.T, ss store.Store) { } r1, err = ss.Post().Get(context.Background(), o1.Id, opts, o1.UserId, map[string]bool{}) require.NoError(t, err) - assert.Len(t, r1.Order, 3) // including the root post + require.Len(t, r1.Order, 3) // including the root post + require.Len(t, r1.Posts, 3) assert.GreaterOrEqual(t, r1.Posts[r1.Order[len(r1.Order)-1]].CreateAt, lastPostCreateAt) assert.False(t, r1.HasNext) @@ -802,7 +804,8 @@ func testPostStoreGetForThread(t *testing.T, ss store.Store) { } r1, err = ss.Post().Get(context.Background(), o1.Id, opts, o1.UserId, map[string]bool{}) require.NoError(t, err) - assert.Len(t, r1.Order, 3) // including the root post + require.Len(t, r1.Order, 3) // including the root post + require.Len(t, r1.Posts, 3) assert.LessOrEqual(t, r1.Posts[r1.Order[1]].CreateAt, firstPostCreateAt) assert.False(t, r1.HasNext) @@ -816,7 +819,8 @@ func testPostStoreGetForThread(t *testing.T, ss store.Store) { } r1, err = ss.Post().Get(context.Background(), o1.Id, opts, o1.UserId, map[string]bool{}) require.NoError(t, err) - assert.Len(t, r1.Order, 2) // including the root post + require.Len(t, r1.Order, 2) // including the root post + require.Len(t, r1.Posts, 2) assert.LessOrEqual(t, r1.Posts[r1.Order[1]].CreateAt, m1.CreateAt) assert.True(t, r1.HasNext) @@ -829,7 +833,8 @@ func testPostStoreGetForThread(t *testing.T, ss store.Store) { } r1, err = ss.Post().Get(context.Background(), o1.Id, opts, o1.UserId, map[string]bool{}) require.NoError(t, err) - assert.Len(t, r1.Order, 2) // including the root post + require.Len(t, r1.Order, 2) // including the root post + require.Len(t, r1.Posts, 2) assert.True(t, r1.HasNext) lastPostID = r1.Order[len(r1.Order)-1] @@ -845,7 +850,8 @@ func testPostStoreGetForThread(t *testing.T, ss store.Store) { } r1, err = ss.Post().Get(context.Background(), o1.Id, opts, o1.UserId, map[string]bool{}) require.NoError(t, err) - assert.Len(t, r1.Order, 4) // including the root post + require.Len(t, r1.Order, 4) // including the root post + require.Len(t, r1.Posts, 4) assert.GreaterOrEqual(t, r1.Posts[r1.Order[len(r1.Order)-1]].CreateAt, lastPostCreateAt) assert.False(t, r1.HasNext) @@ -861,7 +867,8 @@ func testPostStoreGetForThread(t *testing.T, ss store.Store) { } r1, err = ss.Post().Get(context.Background(), o1.Id, opts, o1.UserId, map[string]bool{}) require.NoError(t, err) - assert.Len(t, r1.Order, 2) // including the root post + require.Len(t, r1.Order, 2) // including the root post + require.Len(t, r1.Posts, 2) assert.LessOrEqual(t, r1.Posts[r1.Order[1]].CreateAt, firstPostCreateAt) assert.False(t, r1.HasNext) @@ -875,7 +882,8 @@ func testPostStoreGetForThread(t *testing.T, ss store.Store) { } r1, err = ss.Post().Get(context.Background(), o1.Id, opts, o1.UserId, map[string]bool{}) require.NoError(t, err) - assert.Len(t, r1.Order, 2) // including the root post + require.Len(t, r1.Order, 2) // including the root post + require.Len(t, r1.Posts, 2) assert.GreaterOrEqual(t, r1.Posts[r1.Order[1]].CreateAt, m1.CreateAt) assert.True(t, r1.HasNext) }) @@ -1411,6 +1419,30 @@ func testPostStorePermDelete1Level(t *testing.T, ss store.Store) { o2, err = ss.Post().Save(o2) require.NoError(t, err) + r1 := &model.Reaction{} + r1.ChannelId = o1.ChannelId + r1.UserId = o2.UserId + r1.PostId = o1.Id + r1.EmojiName = "smile" + r1, err = ss.Reaction().Save(r1) + require.NoError(t, err) + + r2 := &model.Reaction{} + r2.ChannelId = o1.ChannelId + r2.UserId = o1.UserId + r2.PostId = o2.Id + r2.EmojiName = "wave" + _, err = ss.Reaction().Save(r2) + require.NoError(t, err) + + r3 := &model.Reaction{} + r3.ChannelId = o1.ChannelId + r3.UserId = model.NewId() + r3.PostId = o1.Id + r3.EmojiName = "sad" + r3, err = ss.Reaction().Save(r3) + require.NoError(t, err) + channel2, err := ss.Channel().Save(&model.Channel{ TeamId: teamId, DisplayName: "DisplayName2", @@ -1425,6 +1457,14 @@ func testPostStorePermDelete1Level(t *testing.T, ss store.Store) { o3, err = ss.Post().Save(o3) require.NoError(t, err) + r4 := &model.Reaction{} + r4.ChannelId = channel2.Id + r4.UserId = model.NewId() + r4.PostId = o3.Id + r4.EmojiName = "angry" + _, err = ss.Reaction().Save(r4) + require.NoError(t, err) + channel3, err := ss.Channel().Save(&model.Channel{ TeamId: teamId, DisplayName: "DisplayName3", @@ -1475,9 +1515,21 @@ func testPostStorePermDelete1Level(t *testing.T, ss store.Store) { _, err = ss.Post().Get(context.Background(), o1.Id, model.GetPostsOptions{}, "", map[string]bool{}) require.NoError(t, err, "Deleted id shouldn't have failed") + reactions, err := ss.Reaction().GetForPost(o1.Id, false) + require.NoError(t, err, "Reactions should exist") + require.Equal(t, 2, len(reactions)) + emojis := []string{r1.EmojiName, r3.EmojiName} + for _, reaction := range reactions { + require.Contains(t, emojis, reaction.EmojiName) + } + _, err = ss.Post().Get(context.Background(), o2.Id, model.GetPostsOptions{}, "", map[string]bool{}) require.Error(t, err, "Deleted id should have failed") + reactions, err = ss.Reaction().GetForPost(o2.Id, false) + require.NoError(t, err, "No error for not found") + require.Equal(t, 0, len(reactions)) + thread, err = ss.Thread().Get(o5.Id) require.NoError(t, err) require.NotEmpty(t, thread) @@ -1489,6 +1541,17 @@ func testPostStorePermDelete1Level(t *testing.T, ss store.Store) { require.NoError(t, err) require.Nil(t, thread) + reactions, err = ss.Reaction().GetForPost(o3.Id, false) + require.NoError(t, err, "No error for not found") + require.Equal(t, 0, len(reactions)) + + reactions, err = ss.Reaction().GetForPost(o1.Id, false) + require.NoError(t, err, "Reactions should exist") + require.Equal(t, 2, len(reactions)) + for _, reaction := range reactions { + require.Contains(t, emojis, reaction.EmojiName) + } + _, err = ss.Post().Get(context.Background(), o3.Id, model.GetPostsOptions{}, "", map[string]bool{}) require.Error(t, err, "Deleted id should have failed") @@ -3926,8 +3989,9 @@ func testPostStorePermanentDeleteBatch(t *testing.T, ss store.Store) { o3, err = ss.Post().Save(o3) require.NoError(t, err) - _, _, err = ss.Post().PermanentDeleteBatchForRetentionPolicies(0, 2000, 1000, model.RetentionPolicyCursor{}) + deleted, _, err := ss.Post().PermanentDeleteBatchForRetentionPolicies(0, 2000, 1000, model.RetentionPolicyCursor{}) require.NoError(t, err) + require.Equal(t, int64(2), deleted) _, err = ss.Post().Get(context.Background(), o1.Id, model.GetPostsOptions{}, "", map[string]bool{}) require.Error(t, err, "Should have not found post 1 after purge") @@ -3938,6 +4002,14 @@ func testPostStorePermanentDeleteBatch(t *testing.T, ss store.Store) { _, err = ss.Post().Get(context.Background(), o3.Id, model.GetPostsOptions{}, "", map[string]bool{}) require.NoError(t, err, "Should have found post 3 after purge") + rows, err := ss.RetentionPolicy().GetIdsForDeletionByTableName("Posts", 1000) + require.NoError(t, err) + require.Equal(t, 1, len(rows)) + require.Equal(t, 2, len(rows[0].Ids)) + // Clean up retention ids table + err = ss.Reaction().DeleteOrphanedRowsByIds(rows[0]) + require.NoError(t, err) + t.Run("with pagination", func(t *testing.T) { for i := 0; i < 3; i++ { _, err = ss.Post().Save(&model.Post{ @@ -3950,13 +4022,31 @@ func testPostStorePermanentDeleteBatch(t *testing.T, ss store.Store) { } cursor := model.RetentionPolicyCursor{} - deleted, cursor, err := ss.Post().PermanentDeleteBatchForRetentionPolicies(0, 2, 2, cursor) + deleted, cursor, err = ss.Post().PermanentDeleteBatchForRetentionPolicies(0, 2, 2, cursor) require.NoError(t, err) require.Equal(t, int64(2), deleted) + rows, err = ss.RetentionPolicy().GetIdsForDeletionByTableName("Posts", 1000) + require.NoError(t, err) + require.Equal(t, 1, len(rows)) + require.Equal(t, 2, len(rows[0].Ids)) + + // Clean up retention ids table + err = ss.Reaction().DeleteOrphanedRowsByIds(rows[0]) + require.NoError(t, err) + deleted, _, err = ss.Post().PermanentDeleteBatchForRetentionPolicies(0, 2, 2, cursor) require.NoError(t, err) require.Equal(t, int64(1), deleted) + + rows, err = ss.RetentionPolicy().GetIdsForDeletionByTableName("Posts", 1000) + require.NoError(t, err) + require.Equal(t, 1, len(rows)) + require.Equal(t, 1, len(rows[0].Ids)) + + // Clean up retention ids table + err = ss.Reaction().DeleteOrphanedRowsByIds(rows[0]) + require.NoError(t, err) }) t.Run("with data retention policies", func(t *testing.T) { @@ -4024,6 +4114,14 @@ func testPostStorePermanentDeleteBatch(t *testing.T, ss store.Store) { err2 = ss.RetentionPolicy().Delete(teamPolicy.ID) require.NoError(t, err2) + + // Clean up retention ids table + rows, err = ss.RetentionPolicy().GetIdsForDeletionByTableName("Posts", 1000) + require.NoError(t, err) + for _, row := range rows { + err = ss.Reaction().DeleteOrphanedRowsByIds(row) + require.NoError(t, err) + } }) t.Run("with channel, team and global policies", func(t *testing.T) { @@ -4089,6 +4187,17 @@ func testPostStorePermanentDeleteBatch(t *testing.T, ss store.Store) { deleted, _, err2 := ss.Post().PermanentDeleteBatchForRetentionPolicies(nowMillis, 2, 1000, model.RetentionPolicyCursor{}) require.NoError(t, err2) require.Equal(t, int64(3), deleted) + + rows, err = ss.RetentionPolicy().GetIdsForDeletionByTableName("Posts", 1000) + require.NoError(t, err) + // Each policy would generate it's own row + require.Equal(t, 3, len(rows)) + + // Clean up retention ids table + for _, row := range rows { + err = ss.Reaction().DeleteOrphanedRowsByIds(row) + require.NoError(t, err) + } }) } @@ -4154,6 +4263,14 @@ func testPostStoreGetParentsForExportAfter(t *testing.T, ss store.Store) { _, nErr := ss.Channel().Save(&c1, -1) require.NoError(t, nErr) + c2 := model.Channel{} + c2.TeamId = t1.Id + c2.DisplayName = "Channel2" + c2.Name = NewTestId() + c2.Type = model.ChannelTypeOpen + _, nErr = ss.Channel().Save(&c2, -1) + require.NoError(t, nErr) + u1 := model.User{} u1.Username = model.NewId() u1.Email = MakeEmail() @@ -4169,21 +4286,56 @@ func testPostStoreGetParentsForExportAfter(t *testing.T, ss store.Store) { p1, nErr = ss.Post().Save(p1) require.NoError(t, nErr) - posts, err := ss.Post().GetParentsForExportAfter(10000, strings.Repeat("0", 26)) - assert.NoError(t, err) + p2 := &model.Post{} + p2.ChannelId = c2.Id + p2.UserId = u1.Id + p2.Message = NewTestId() + p2.CreateAt = 1000 + p2, nErr = ss.Post().Save(p2) + require.NoError(t, nErr) + nErr = ss.Channel().Delete(c2.Id, model.GetMillis()) + require.NoError(t, nErr) - found := false - for _, p := range posts { - if p.Id == p1.Id { - found = true - assert.Equal(t, p.Id, p1.Id) - assert.Equal(t, p.Message, p1.Message) - assert.Equal(t, p.Username, u1.Username) - assert.Equal(t, p.TeamName, t1.Name) - assert.Equal(t, p.ChannelName, c1.Name) + t.Run("without archived channels", func(t *testing.T) { + posts, err := ss.Post().GetParentsForExportAfter(10000, strings.Repeat("0", 26), false) + assert.NoError(t, err) + + found := false + foundArchived := false + for _, p := range posts { + if p.Id == p1.Id { + found = true + assert.Equal(t, p.Id, p1.Id) + assert.Equal(t, p.Message, p1.Message) + assert.Equal(t, p.Username, u1.Username) + assert.Equal(t, p.TeamName, t1.Name) + assert.Equal(t, p.ChannelName, c1.Name) + } + if p.Id == p2.Id { + foundArchived = true + } } - } - assert.True(t, found) + assert.True(t, found) + assert.False(t, foundArchived, "posts from archived channel should not be returned") + }) + + t.Run("with archived channels", func(t *testing.T) { + posts, err := ss.Post().GetParentsForExportAfter(10000, strings.Repeat("0", 26), true) + assert.NoError(t, err) + + found := false + for _, p := range posts { + if p.Id == p2.Id { + found = true + assert.Equal(t, p.Id, p2.Id) + assert.Equal(t, p.Message, p2.Message) + assert.Equal(t, p.Username, u1.Username) + assert.Equal(t, p.TeamName, t1.Name) + assert.Equal(t, p.ChannelName, c2.Name) + } + } + assert.True(t, found) + }) } func testPostStoreGetRepliesForExport(t *testing.T, ss store.Store) { diff --git a/server/channels/store/storetest/preference_store.go b/server/channels/store/storetest/preference_store.go index 8cf55985349..ad2dcc25534 100644 --- a/server/channels/store/storetest/preference_store.go +++ b/server/channels/store/storetest/preference_store.go @@ -384,6 +384,14 @@ func testPreferenceDeleteOrphanedRows(t *testing.T, ss store.Store) { _, _, nErr = ss.Post().PermanentDeleteBatchForRetentionPolicies(0, 2000, limit, model.RetentionPolicyCursor{}) assert.NoError(t, nErr) + rows, err := ss.RetentionPolicy().GetIdsForDeletionByTableName("Posts", 1000) + require.NoError(t, err) + require.Equal(t, 1, len(rows)) + + // Clean up retention ids table + err = ss.Reaction().DeleteOrphanedRowsByIds(rows[0]) + require.NoError(t, err) + _, nErr = ss.Preference().DeleteOrphanedRows(limit) assert.NoError(t, nErr) diff --git a/server/channels/store/storetest/reaction_store.go b/server/channels/store/storetest/reaction_store.go index 56bf158c3e2..0b4b3cb4d0a 100644 --- a/server/channels/store/storetest/reaction_store.go +++ b/server/channels/store/storetest/reaction_store.go @@ -24,6 +24,7 @@ func TestReactionStore(t *testing.T, ss store.Store, s SqlStore) { t.Run("ReactionGetForPost", func(t *testing.T) { testReactionGetForPost(t, ss) }) t.Run("ReactionGetForPostSince", func(t *testing.T) { testReactionGetForPostSince(t, ss, s) }) t.Run("ReactionDeleteAllWithEmojiName", func(t *testing.T) { testReactionDeleteAllWithEmojiName(t, ss, s) }) + t.Run("PermanentDeleteByUser", func(t *testing.T) { testPermanentDeleteByUser(t, ss) }) t.Run("PermanentDeleteBatch", func(t *testing.T) { testReactionStorePermanentDeleteBatch(t, ss) }) t.Run("ReactionBulkGetForPosts", func(t *testing.T) { testReactionBulkGetForPosts(t, ss) }) t.Run("ReactionDeadlock", func(t *testing.T) { testReactionDeadlock(t, ss) }) @@ -563,6 +564,91 @@ func testReactionDeleteAllWithEmojiName(t *testing.T, ss store.Store, s SqlStore } +func testPermanentDeleteByUser(t *testing.T, ss store.Store) { + userId := model.NewId() + post, err1 := ss.Post().Save(&model.Post{ + ChannelId: model.NewId(), + UserId: model.NewId(), + }) + require.NoError(t, err1) + post2, err2 := ss.Post().Save(&model.Post{ + ChannelId: model.NewId(), + UserId: model.NewId(), + }) + require.NoError(t, err2) + post3, err3 := ss.Post().Save(&model.Post{ + ChannelId: model.NewId(), + UserId: model.NewId(), + }) + require.NoError(t, err3) + + reactions := []*model.Reaction{ + { + UserId: userId, + PostId: post.Id, + EmojiName: "happy", + }, + { + UserId: model.NewId(), + PostId: post.Id, + EmojiName: "smile", + }, + { + UserId: model.NewId(), + PostId: post.Id, + EmojiName: "sad", + }, + { + UserId: userId, + PostId: post2.Id, + EmojiName: "angry", + }, + { + UserId: userId, + PostId: post3.Id, + EmojiName: "joy", + }, + } + + for _, reaction := range reactions { + _, err := ss.Reaction().Save(reaction) + require.NoError(t, err) + } + + err := ss.Reaction().PermanentDeleteByUser(userId) + require.NoError(t, err) + + // check that the reactions were deleted + returned, err := ss.Reaction().GetForPost(post.Id, false) + require.NoError(t, err) + require.Len(t, returned, 2, "should only have removed reaction for user") + + for _, reaction := range returned { + assert.NotEqual(t, reaction.EmojiName, "happy", "should've removed reaction with emoji name") + } + + returned, err = ss.Reaction().GetForPost(post2.Id, false) + require.NoError(t, err) + require.Len(t, returned, 0, "should have removed reaction for user") + + returned, err = ss.Reaction().GetForPost(post3.Id, false) + require.NoError(t, err) + require.Len(t, returned, 0, "should remove reaction for user") + + // check that the posts are updated + postList, err := ss.Post().Get(context.Background(), post.Id, model.GetPostsOptions{}, "", map[string]bool{}) + require.NoError(t, err) + assert.True(t, postList.Posts[post.Id].HasReactions, "post should still have reactions") + + postList, err = ss.Post().Get(context.Background(), post2.Id, model.GetPostsOptions{}, "", map[string]bool{}) + require.NoError(t, err) + assert.False(t, postList.Posts[post2.Id].HasReactions, "post shouldn't have reactions any more") + + postList, err = ss.Post().Get(context.Background(), post3.Id, model.GetPostsOptions{}, "", map[string]bool{}) + require.NoError(t, err) + assert.False(t, postList.Posts[post3.Id].HasReactions, "post shouldn't have reactions any more") +} + func testReactionStorePermanentDeleteBatch(t *testing.T, ss store.Store) { const limit = 1000 team, err := ss.Team().Save(&model.Team{ @@ -620,8 +706,20 @@ func testReactionStorePermanentDeleteBatch(t *testing.T, ss store.Store) { _, _, err = ss.Post().PermanentDeleteBatchForRetentionPolicies(0, 2000, limit, model.RetentionPolicyCursor{}) require.NoError(t, err) - _, err = ss.Reaction().DeleteOrphanedRows(limit) + rows, err := ss.RetentionPolicy().GetIdsForDeletionByTableName("Posts", 1000) require.NoError(t, err) + require.Equal(t, 1, len(rows)) + require.Equal(t, 1, len(rows[0].Ids)) + require.Contains(t, rows[0].Ids, olderPost.Id) + + for _, row := range rows { + err = ss.Reaction().DeleteOrphanedRowsByIds(row) + require.NoError(t, err) + } + + rows, err = ss.RetentionPolicy().GetIdsForDeletionByTableName("Posts", 1000) + require.NoError(t, err) + require.Equal(t, 0, len(rows)) returned, err := ss.Reaction().GetForPost(olderPost.Id, false) require.NoError(t, err) diff --git a/server/channels/store/storetest/store.go b/server/channels/store/storetest/store.go index 80fa81159a9..c0da42ac9bb 100644 --- a/server/channels/store/storetest/store.go +++ b/server/channels/store/storetest/store.go @@ -61,6 +61,7 @@ type Store struct { PostAcknowledgementStore mocks.PostAcknowledgementStore PostPersistentNotificationStore mocks.PostPersistentNotificationStore TrueUpReviewStore mocks.TrueUpReviewStore + DesktopTokensStore mocks.DesktopTokensStore } func (s *Store) SetContext(context context.Context) { s.context = context } @@ -103,6 +104,7 @@ func (s *Store) ChannelMemberHistory() store.ChannelMemberHistoryStore { return &s.ChannelMemberHistoryStore } func (s *Store) TrueUpReview() store.TrueUpReviewStore { return &s.TrueUpReviewStore } +func (s *Store) DesktopTokens() store.DesktopTokensStore { return &s.DesktopTokensStore } func (s *Store) NotifyAdmin() store.NotifyAdminStore { return &s.NotifyAdminStore } func (s *Store) Group() store.GroupStore { return &s.GroupStore } func (s *Store) LinkMetadata() store.LinkMetadataStore { return &s.LinkMetadataStore } @@ -177,5 +179,6 @@ func (s *Store) AssertExpectations(t mock.TestingT) bool { &s.PostPriorityStore, &s.PostAcknowledgementStore, &s.PostPersistentNotificationStore, + &s.DesktopTokensStore, ) } diff --git a/server/channels/store/storetest/team_store.go b/server/channels/store/storetest/team_store.go index ed706c1e9b3..53d92f95117 100644 --- a/server/channels/store/storetest/team_store.go +++ b/server/channels/store/storetest/team_store.go @@ -73,6 +73,7 @@ func TestTeamStore(t *testing.T, ss store.Store) { t.Run("GetTeamMembersForExport", func(t *testing.T) { testTeamStoreGetTeamMembersForExport(t, ss) }) t.Run("GetTeamsForUserWithPagination", func(t *testing.T) { testTeamMembersWithPagination(t, ss) }) t.Run("GroupSyncedTeamCount", func(t *testing.T) { testGroupSyncedTeamCount(t, ss) }) + t.Run("GetCommonTeamIDsForMultipleUsers", func(t *testing.T) { testGetCommonTeamIDsForMultipleUsers(t, ss) }) } func testTeamStoreSave(t *testing.T, ss store.Store) { @@ -3618,3 +3619,203 @@ func testGroupSyncedTeamCount(t *testing.T, ss store.Store) { require.NoError(t, err) require.GreaterOrEqual(t, countAfter, count+1) } + +func testGetCommonTeamIDsForMultipleUsers(t *testing.T, ss store.Store) { + // Creating teams + + // Team 1 + t1 := model.Team{} + t1.DisplayName = "Team 1" + t1.Name = NewTestId() + t1.Email = MakeEmail() + t1.Type = model.TeamOpen + _, err := ss.Team().Save(&t1) + require.NoError(t, err) + + // Team 2 + t2 := model.Team{} + t2.DisplayName = "Team 2" + t2.Name = NewTestId() + t2.Email = MakeEmail() + t2.Type = model.TeamOpen + _, err = ss.Team().Save(&t2) + require.NoError(t, err) + + // Team 3 + t3 := model.Team{} + t3.DisplayName = "Team 3" + t3.Name = NewTestId() + t3.Email = MakeEmail() + t3.Type = model.TeamOpen + _, err = ss.Team().Save(&t3) + require.NoError(t, err) + + // Creating users + + // User 1 + u1 := model.User{} + u1.Email = MakeEmail() + u1.Nickname = NewTestId() + _, err = ss.User().Save(&u1) + require.NoError(t, err) + + // User 2 + u2 := model.User{} + u2.Email = MakeEmail() + u2.Nickname = NewTestId() + _, err = ss.User().Save(&u2) + require.NoError(t, err) + + t.Run("multiple common teams exist", func(t *testing.T) { + // Add user 1 in team 1 and 2 + m1 := &model.TeamMember{TeamId: t1.Id, UserId: u1.Id} + m2 := &model.TeamMember{TeamId: t2.Id, UserId: u1.Id} + + // Add user 2 in team1, 2 and 3 + m3 := &model.TeamMember{TeamId: t1.Id, UserId: u2.Id} + m4 := &model.TeamMember{TeamId: t2.Id, UserId: u2.Id} + m5 := &model.TeamMember{TeamId: t3.Id, UserId: u2.Id} + + // Save team memberships + _, nErr := ss.Team().SaveMultipleMembers([]*model.TeamMember{m1, m2, m3, m4, m5}, -1) + require.NoError(t, nErr) + + // Find common teams between user 1 and user 2 + commonTeamIDs, err2 := ss.Team().GetCommonTeamIDsForMultipleUsers([]string{u1.Id, u2.Id}) + require.NoError(t, err2) + require.Equal(t, 2, len(commonTeamIDs)) + require.Contains(t, commonTeamIDs, t1.Id) + require.Contains(t, commonTeamIDs, t2.Id) + + // cleanup + err2 = ss.Team().RemoveAllMembersByUser(u1.Id) + require.NoError(t, err2) + + err2 = ss.Team().RemoveAllMembersByUser(u2.Id) + require.NoError(t, err2) + }) + + t.Run("single common teams exist", func(t *testing.T) { + // Add user 1 in team 1 and 2 + m1 := &model.TeamMember{TeamId: t1.Id, UserId: u1.Id} + m2 := &model.TeamMember{TeamId: t2.Id, UserId: u1.Id} + + // Add user 2 in team1, 2 and 3 + m3 := &model.TeamMember{TeamId: t1.Id, UserId: u2.Id} + m5 := &model.TeamMember{TeamId: t3.Id, UserId: u2.Id} + + // Save team memberships + _, nErr := ss.Team().SaveMultipleMembers([]*model.TeamMember{m1, m2, m3, m5}, -1) + require.NoError(t, nErr) + + // Find common teams between user 1 and user 2 + commonTeamIDs, err2 := ss.Team().GetCommonTeamIDsForMultipleUsers([]string{u1.Id, u2.Id}) + require.NoError(t, err2) + require.Equal(t, 1, len(commonTeamIDs)) + require.Contains(t, commonTeamIDs, t1.Id) + + // cleanup + err2 = ss.Team().RemoveAllMembersByUser(u1.Id) + require.NoError(t, err2) + + err2 = ss.Team().RemoveAllMembersByUser(u2.Id) + require.NoError(t, err2) + }) + + t.Run("no common teams exist", func(t *testing.T) { + // Add user 1 in team 1 and 2 + m1 := &model.TeamMember{TeamId: t1.Id, UserId: u1.Id} + + // Add user 2 in team1, 2 and 3 + m4 := &model.TeamMember{TeamId: t2.Id, UserId: u2.Id} + m5 := &model.TeamMember{TeamId: t3.Id, UserId: u2.Id} + + // Save team memberships + _, nErr := ss.Team().SaveMultipleMembers([]*model.TeamMember{m1, m4, m5}, -1) + require.NoError(t, nErr) + + // Find common teams between user 1 and user 2 + commonTeamIDs, err2 := ss.Team().GetCommonTeamIDsForMultipleUsers([]string{u1.Id, u2.Id}) + require.NoError(t, err2) + require.Equal(t, 0, len(commonTeamIDs)) + + // cleanup + err2 = ss.Team().RemoveAllMembersByUser(u1.Id) + require.NoError(t, err2) + + err2 = ss.Team().RemoveAllMembersByUser(u2.Id) + require.NoError(t, err2) + }) + + t.Run("some user have no team members", func(t *testing.T) { + // Add user 1 in team 1 and 2 + m1 := &model.TeamMember{TeamId: t1.Id, UserId: u1.Id} + m2 := &model.TeamMember{TeamId: t2.Id, UserId: u1.Id} + + // We'll leave user 2 without any team + + // Save team memberships + _, nErr := ss.Team().SaveMultipleMembers([]*model.TeamMember{m1, m2}, -1) + require.NoError(t, nErr) + + // Find common teams between user 1 and user 2 + commonTeamIDs, err2 := ss.Team().GetCommonTeamIDsForMultipleUsers([]string{u1.Id, u2.Id}) + require.NoError(t, err2) + require.Equal(t, 0, len(commonTeamIDs)) + + // cleanup + err2 = ss.Team().RemoveAllMembersByUser(u1.Id) + require.NoError(t, err2) + }) + + t.Run("more than two users, common teams exist", func(t *testing.T) { + // User 3 + u3 := model.User{} + u3.Email = MakeEmail() + u3.Nickname = NewTestId() + _, err = ss.User().Save(&u3) + require.NoError(t, err) + + // User 4 + u4 := model.User{} + u4.Email = MakeEmail() + u4.Nickname = NewTestId() + _, err = ss.User().Save(&u4) + require.NoError(t, err) + + // Add user 1 in team 1 and 2 + m1 := &model.TeamMember{TeamId: t1.Id, UserId: u1.Id} + m2 := &model.TeamMember{TeamId: t2.Id, UserId: u1.Id} + + // Add user 2 in team1, 2 and 3 + m3 := &model.TeamMember{TeamId: t1.Id, UserId: u2.Id} + m4 := &model.TeamMember{TeamId: t2.Id, UserId: u2.Id} + m5 := &model.TeamMember{TeamId: t3.Id, UserId: u2.Id} + + // Add user 3 in team 1 and 2 + m6 := &model.TeamMember{TeamId: t1.Id, UserId: u3.Id} + m7 := &model.TeamMember{TeamId: t2.Id, UserId: u3.Id} + + // Add user 4 in team 1 and 2 + m8 := &model.TeamMember{TeamId: t1.Id, UserId: u4.Id} + m9 := &model.TeamMember{TeamId: t2.Id, UserId: u4.Id} + + // Save team memberships + _, nErr := ss.Team().SaveMultipleMembers([]*model.TeamMember{m1, m2, m3, m4, m5, m6, m7, m8, m9}, -1) + require.NoError(t, nErr) + + // Find common teams between user 1 and user 2 + commonTeamIDs, err := ss.Team().GetCommonTeamIDsForMultipleUsers([]string{u1.Id, u2.Id, u3.Id, u4.Id}) + require.NoError(t, err) + require.Equal(t, 2, len(commonTeamIDs)) + require.Contains(t, commonTeamIDs, t1.Id) + require.Contains(t, commonTeamIDs, t2.Id) + + // cleanup + err = ss.Team().RemoveAllMembersByUser(u1.Id) + require.NoError(t, err) + + err = ss.Team().RemoveAllMembersByUser(u2.Id) + require.NoError(t, err) + }) +} diff --git a/server/channels/store/storetest/user_store.go b/server/channels/store/storetest/user_store.go index 66212f47b9d..baf55221930 100644 --- a/server/channels/store/storetest/user_store.go +++ b/server/channels/store/storetest/user_store.go @@ -3963,6 +3963,15 @@ func testCount(t *testing.T, ss store.Store) { require.NoError(t, err) defer func() { require.NoError(t, ss.User().PermanentDelete(deletedUser.Id)) }() + // Remote User + remoteId := "remote-id" + remoteUser, err := ss.User().Save(&model.User{ + Email: MakeEmail(), + RemoteId: &remoteId, + }) + require.NoError(t, err) + defer func() { require.NoError(t, ss.User().PermanentDelete(remoteUser.Id)) }() + // Bot botUser, err := ss.User().Save(&model.User{ Email: MakeEmail(), @@ -4067,6 +4076,71 @@ func testCount(t *testing.T, ss store.Store) { }, 0, }, + { + "Include remote accounts no deleted accounts and no team id", + model.UserCountOptions{ + IncludeRemoteUsers: true, + IncludeDeleted: false, + TeamId: "", + }, + 5, + }, + { + "Include delete accounts no remote accounts and no team id", + model.UserCountOptions{ + IncludeRemoteUsers: false, + IncludeDeleted: true, + TeamId: "", + }, + 5, + }, + { + "Include remote accounts and deleted accounts and no team id", + model.UserCountOptions{ + IncludeRemoteUsers: true, + IncludeDeleted: true, + TeamId: "", + }, + 6, + }, + { + "Include remote accounts and deleted accounts with existing team id", + model.UserCountOptions{ + IncludeRemoteUsers: true, + IncludeDeleted: true, + TeamId: teamId, + }, + 4, + }, + { + "Include remote accounts and deleted accounts with fake team id", + model.UserCountOptions{ + IncludeRemoteUsers: true, + IncludeDeleted: true, + TeamId: model.NewId(), + }, + 0, + }, + { + "Include remote accounts and deleted accounts with existing team id and view restrictions allowing team", + model.UserCountOptions{ + IncludeRemoteUsers: true, + IncludeDeleted: true, + TeamId: teamId, + ViewRestrictions: &model.ViewUsersRestrictions{Teams: []string{teamId}}, + }, + 4, + }, + { + "Include remote accounts and deleted accounts with existing team id and view restrictions not allowing current team", + model.UserCountOptions{ + IncludeRemoteUsers: true, + IncludeDeleted: true, + TeamId: teamId, + ViewRestrictions: &model.ViewUsersRestrictions{Teams: []string{model.NewId()}}, + }, + 0, + }, { "Filter by system admins only", model.UserCountOptions{ diff --git a/server/channels/store/timerlayer/timerlayer.go b/server/channels/store/timerlayer/timerlayer.go index e12927a33c8..2b99cb78167 100644 --- a/server/channels/store/timerlayer/timerlayer.go +++ b/server/channels/store/timerlayer/timerlayer.go @@ -11,6 +11,7 @@ import ( "time" "github.com/mattermost/mattermost/server/public/model" + "github.com/mattermost/mattermost/server/public/shared/request" "github.com/mattermost/mattermost/server/v8/channels/store" "github.com/mattermost/mattermost/server/v8/einterfaces" ) @@ -26,6 +27,7 @@ type TimerLayer struct { CommandStore store.CommandStore CommandWebhookStore store.CommandWebhookStore ComplianceStore store.ComplianceStore + DesktopTokensStore store.DesktopTokensStore DraftStore store.DraftStore EmojiStore store.EmojiStore FileInfoStore store.FileInfoStore @@ -95,6 +97,10 @@ func (s *TimerLayer) Compliance() store.ComplianceStore { return s.ComplianceStore } +func (s *TimerLayer) DesktopTokens() store.DesktopTokensStore { + return s.DesktopTokensStore +} + func (s *TimerLayer) Draft() store.DraftStore { return s.DraftStore } @@ -275,6 +281,11 @@ type TimerLayerComplianceStore struct { Root *TimerLayer } +type TimerLayerDesktopTokensStore struct { + store.DesktopTokensStore + Root *TimerLayer +} + type TimerLayerDraftStore struct { store.DraftStore Root *TimerLayer @@ -816,6 +827,22 @@ func (s *TimerLayerChannelStore) Delete(channelID string, timestamp int64) error return err } +func (s *TimerLayerChannelStore) DeleteAllSidebarChannelForChannel(channelID string) error { + start := time.Now() + + err := s.ChannelStore.DeleteAllSidebarChannelForChannel(channelID) + + elapsed := float64(time.Since(start)) / float64(time.Second) + if s.Root.Metrics != nil { + success := "false" + if err == nil { + success = "true" + } + s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.DeleteAllSidebarChannelForChannel", success, elapsed) + } + return err +} + func (s *TimerLayerChannelStore) DeleteSidebarCategory(categoryID string) error { start := time.Now() @@ -1040,6 +1067,22 @@ func (s *TimerLayerChannelStore) GetByNames(team_id string, names []string, allo return result, err } +func (s *TimerLayerChannelStore) GetByNamesIncludeDeleted(team_id string, names []string, allowFromCache bool) ([]*model.Channel, error) { + start := time.Now() + + result, err := s.ChannelStore.GetByNamesIncludeDeleted(team_id, names, allowFromCache) + + elapsed := float64(time.Since(start)) / float64(time.Second) + if s.Root.Metrics != nil { + success := "false" + if err == nil { + success = "true" + } + s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.GetByNamesIncludeDeleted", success, elapsed) + } + return result, err +} + func (s *TimerLayerChannelStore) GetChannelCounts(teamID string, userID string) (*model.ChannelCounts, error) { start := time.Now() @@ -1056,10 +1099,10 @@ func (s *TimerLayerChannelStore) GetChannelCounts(teamID string, userID string) return result, err } -func (s *TimerLayerChannelStore) GetChannelMembersForExport(userID string, teamID string) ([]*model.ChannelMemberForExport, error) { +func (s *TimerLayerChannelStore) GetChannelMembersForExport(userID string, teamID string, includeArchivedChannel bool) ([]*model.ChannelMemberForExport, error) { start := time.Now() - result, err := s.ChannelStore.GetChannelMembersForExport(userID, teamID) + result, err := s.ChannelStore.GetChannelMembersForExport(userID, teamID, includeArchivedChannel) elapsed := float64(time.Since(start)) / float64(time.Second) if s.Root.Metrics != nil { @@ -1232,6 +1275,22 @@ func (s *TimerLayerChannelStore) GetChannelsWithTeamDataByIds(channelIds []strin return result, err } +func (s *TimerLayerChannelStore) GetChannelsWithUnreadsAndWithMentions(ctx context.Context, channelIDs []string, userID string, userNotifyProps model.StringMap) ([]string, []string, map[string]int64, error) { + start := time.Now() + + result, resultVar1, resultVar2, err := s.ChannelStore.GetChannelsWithUnreadsAndWithMentions(ctx, channelIDs, userID, userNotifyProps) + + elapsed := float64(time.Since(start)) / float64(time.Second) + if s.Root.Metrics != nil { + success := "false" + if err == nil { + success = "true" + } + s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.GetChannelsWithUnreadsAndWithMentions", success, elapsed) + } + return result, resultVar1, resultVar2, err +} + func (s *TimerLayerChannelStore) GetDeleted(team_id string, offset int, limit int, userID string) (model.ChannelList, error) { start := time.Now() @@ -2936,6 +2995,86 @@ func (s *TimerLayerComplianceStore) Update(compliance *model.Compliance) (*model return result, err } +func (s *TimerLayerDesktopTokensStore) Delete(token string) error { + start := time.Now() + + err := s.DesktopTokensStore.Delete(token) + + elapsed := float64(time.Since(start)) / float64(time.Second) + if s.Root.Metrics != nil { + success := "false" + if err == nil { + success = "true" + } + s.Root.Metrics.ObserveStoreMethodDuration("DesktopTokensStore.Delete", success, elapsed) + } + return err +} + +func (s *TimerLayerDesktopTokensStore) DeleteByUserId(userId string) error { + start := time.Now() + + err := s.DesktopTokensStore.DeleteByUserId(userId) + + elapsed := float64(time.Since(start)) / float64(time.Second) + if s.Root.Metrics != nil { + success := "false" + if err == nil { + success = "true" + } + s.Root.Metrics.ObserveStoreMethodDuration("DesktopTokensStore.DeleteByUserId", success, elapsed) + } + return err +} + +func (s *TimerLayerDesktopTokensStore) DeleteOlderThan(minCreatedAt int64) error { + start := time.Now() + + err := s.DesktopTokensStore.DeleteOlderThan(minCreatedAt) + + elapsed := float64(time.Since(start)) / float64(time.Second) + if s.Root.Metrics != nil { + success := "false" + if err == nil { + success = "true" + } + s.Root.Metrics.ObserveStoreMethodDuration("DesktopTokensStore.DeleteOlderThan", success, elapsed) + } + return err +} + +func (s *TimerLayerDesktopTokensStore) GetUserId(token string, minCreatedAt int64) (*string, error) { + start := time.Now() + + result, err := s.DesktopTokensStore.GetUserId(token, minCreatedAt) + + elapsed := float64(time.Since(start)) / float64(time.Second) + if s.Root.Metrics != nil { + success := "false" + if err == nil { + success = "true" + } + s.Root.Metrics.ObserveStoreMethodDuration("DesktopTokensStore.GetUserId", success, elapsed) + } + return result, err +} + +func (s *TimerLayerDesktopTokensStore) Insert(token string, createAt int64, userId string) error { + start := time.Now() + + err := s.DesktopTokensStore.Insert(token, createAt, userId) + + elapsed := float64(time.Since(start)) / float64(time.Second) + if s.Root.Metrics != nil { + success := "false" + if err == nil { + success = "true" + } + s.Root.Metrics.ObserveStoreMethodDuration("DesktopTokensStore.Insert", success, elapsed) + } + return err +} + func (s *TimerLayerDraftStore) Delete(userID string, channelID string, rootID string) error { start := time.Now() @@ -3016,7 +3155,7 @@ func (s *TimerLayerEmojiStore) Delete(emoji *model.Emoji, timestamp int64) error return err } -func (s *TimerLayerEmojiStore) Get(ctx context.Context, id string, allowFromCache bool) (*model.Emoji, error) { +func (s *TimerLayerEmojiStore) Get(ctx request.CTX, id string, allowFromCache bool) (*model.Emoji, error) { start := time.Now() result, err := s.EmojiStore.Get(ctx, id, allowFromCache) @@ -3032,7 +3171,7 @@ func (s *TimerLayerEmojiStore) Get(ctx context.Context, id string, allowFromCach return result, err } -func (s *TimerLayerEmojiStore) GetByName(ctx context.Context, name string, allowFromCache bool) (*model.Emoji, error) { +func (s *TimerLayerEmojiStore) GetByName(ctx request.CTX, name string, allowFromCache bool) (*model.Emoji, error) { start := time.Now() result, err := s.EmojiStore.GetByName(ctx, name, allowFromCache) @@ -3064,7 +3203,7 @@ func (s *TimerLayerEmojiStore) GetList(offset int, limit int, sort string) ([]*m return result, err } -func (s *TimerLayerEmojiStore) GetMultipleByName(ctx context.Context, names []string) ([]*model.Emoji, error) { +func (s *TimerLayerEmojiStore) GetMultipleByName(ctx request.CTX, names []string) ([]*model.Emoji, error) { start := time.Now() result, err := s.EmojiStore.GetMultipleByName(ctx, names) @@ -4358,10 +4497,10 @@ func (s *TimerLayerJobStore) Delete(id string) (string, error) { return result, err } -func (s *TimerLayerJobStore) Get(id string) (*model.Job, error) { +func (s *TimerLayerJobStore) Get(c *request.Context, id string) (*model.Job, error) { start := time.Now() - result, err := s.JobStore.Get(id) + result, err := s.JobStore.Get(c, id) elapsed := float64(time.Since(start)) / float64(time.Second) if s.Root.Metrics != nil { @@ -4374,10 +4513,10 @@ func (s *TimerLayerJobStore) Get(id string) (*model.Job, error) { return result, err } -func (s *TimerLayerJobStore) GetAllByStatus(status string) ([]*model.Job, error) { +func (s *TimerLayerJobStore) GetAllByStatus(c *request.Context, status string) ([]*model.Job, error) { start := time.Now() - result, err := s.JobStore.GetAllByStatus(status) + result, err := s.JobStore.GetAllByStatus(c, status) elapsed := float64(time.Since(start)) / float64(time.Second) if s.Root.Metrics != nil { @@ -4390,10 +4529,10 @@ func (s *TimerLayerJobStore) GetAllByStatus(status string) ([]*model.Job, error) return result, err } -func (s *TimerLayerJobStore) GetAllByType(jobType string) ([]*model.Job, error) { +func (s *TimerLayerJobStore) GetAllByType(c *request.Context, jobType string) ([]*model.Job, error) { start := time.Now() - result, err := s.JobStore.GetAllByType(jobType) + result, err := s.JobStore.GetAllByType(c, jobType) elapsed := float64(time.Since(start)) / float64(time.Second) if s.Root.Metrics != nil { @@ -4406,10 +4545,10 @@ func (s *TimerLayerJobStore) GetAllByType(jobType string) ([]*model.Job, error) return result, err } -func (s *TimerLayerJobStore) GetAllByTypeAndStatus(jobType string, status string) ([]*model.Job, error) { +func (s *TimerLayerJobStore) GetAllByTypeAndStatus(c *request.Context, jobType string, status string) ([]*model.Job, error) { start := time.Now() - result, err := s.JobStore.GetAllByTypeAndStatus(jobType, status) + result, err := s.JobStore.GetAllByTypeAndStatus(c, jobType, status) elapsed := float64(time.Since(start)) / float64(time.Second) if s.Root.Metrics != nil { @@ -4422,10 +4561,10 @@ func (s *TimerLayerJobStore) GetAllByTypeAndStatus(jobType string, status string return result, err } -func (s *TimerLayerJobStore) GetAllByTypePage(jobType string, offset int, limit int) ([]*model.Job, error) { +func (s *TimerLayerJobStore) GetAllByTypePage(c *request.Context, jobType string, offset int, limit int) ([]*model.Job, error) { start := time.Now() - result, err := s.JobStore.GetAllByTypePage(jobType, offset, limit) + result, err := s.JobStore.GetAllByTypePage(c, jobType, offset, limit) elapsed := float64(time.Since(start)) / float64(time.Second) if s.Root.Metrics != nil { @@ -4438,10 +4577,10 @@ func (s *TimerLayerJobStore) GetAllByTypePage(jobType string, offset int, limit return result, err } -func (s *TimerLayerJobStore) GetAllByTypesPage(jobTypes []string, offset int, limit int) ([]*model.Job, error) { +func (s *TimerLayerJobStore) GetAllByTypesPage(c *request.Context, jobTypes []string, offset int, limit int) ([]*model.Job, error) { start := time.Now() - result, err := s.JobStore.GetAllByTypesPage(jobTypes, offset, limit) + result, err := s.JobStore.GetAllByTypesPage(c, jobTypes, offset, limit) elapsed := float64(time.Since(start)) / float64(time.Second) if s.Root.Metrics != nil { @@ -4454,22 +4593,6 @@ func (s *TimerLayerJobStore) GetAllByTypesPage(jobTypes []string, offset int, li return result, err } -func (s *TimerLayerJobStore) GetAllPage(offset int, limit int) ([]*model.Job, error) { - start := time.Now() - - result, err := s.JobStore.GetAllPage(offset, limit) - - elapsed := float64(time.Since(start)) / float64(time.Second) - if s.Root.Metrics != nil { - success := "false" - if err == nil { - success = "true" - } - s.Root.Metrics.ObserveStoreMethodDuration("JobStore.GetAllPage", success, elapsed) - } - return result, err -} - func (s *TimerLayerJobStore) GetCountByStatusAndType(status string, jobType string) (int64, error) { start := time.Now() @@ -4598,10 +4721,10 @@ func (s *TimerLayerJobStore) UpdateStatusOptimistically(id string, currentStatus return result, err } -func (s *TimerLayerLicenseStore) Get(id string) (*model.LicenseRecord, error) { +func (s *TimerLayerLicenseStore) Get(ctx context.Context, id string) (*model.LicenseRecord, error) { start := time.Now() - result, err := s.LicenseStore.Get(id) + result, err := s.LicenseStore.Get(ctx, id) elapsed := float64(time.Since(start)) / float64(time.Second) if s.Root.Metrics != nil { @@ -4630,10 +4753,10 @@ func (s *TimerLayerLicenseStore) GetAll() ([]*model.LicenseRecord, error) { return result, err } -func (s *TimerLayerLicenseStore) Save(license *model.LicenseRecord) (*model.LicenseRecord, error) { +func (s *TimerLayerLicenseStore) Save(license *model.LicenseRecord) error { start := time.Now() - result, err := s.LicenseStore.Save(license) + err := s.LicenseStore.Save(license) elapsed := float64(time.Since(start)) / float64(time.Second) if s.Root.Metrics != nil { @@ -4643,7 +4766,7 @@ func (s *TimerLayerLicenseStore) Save(license *model.LicenseRecord) (*model.Lice } s.Root.Metrics.ObserveStoreMethodDuration("LicenseStore.Save", success, elapsed) } - return result, err + return err } func (s *TimerLayerLinkMetadataStore) Get(url string, timestamp int64) (*model.LinkMetadata, error) { @@ -5317,22 +5440,6 @@ func (s *TimerLayerPostStore) Delete(postID string, timestamp int64, deleteByID return err } -func (s *TimerLayerPostStore) DeleteOrphanedRows(limit int) (int64, error) { - start := time.Now() - - result, err := s.PostStore.DeleteOrphanedRows(limit) - - elapsed := float64(time.Since(start)) / float64(time.Second) - if s.Root.Metrics != nil { - success := "false" - if err == nil { - success = "true" - } - s.Root.Metrics.ObserveStoreMethodDuration("PostStore.DeleteOrphanedRows", success, elapsed) - } - return result, err -} - func (s *TimerLayerPostStore) Get(ctx context.Context, id string, opts model.GetPostsOptions, userID string, sanitizeOptions map[string]bool) (*model.PostList, error) { start := time.Now() @@ -5509,10 +5616,10 @@ func (s *TimerLayerPostStore) GetOldestEntityCreationTime() (int64, error) { return result, err } -func (s *TimerLayerPostStore) GetParentsForExportAfter(limit int, afterID string) ([]*model.PostForExport, error) { +func (s *TimerLayerPostStore) GetParentsForExportAfter(limit int, afterID string, includeArchivedChannels bool) ([]*model.PostForExport, error) { start := time.Now() - result, err := s.PostStore.GetParentsForExportAfter(limit, afterID) + result, err := s.PostStore.GetParentsForExportAfter(limit, afterID, includeArchivedChannels) elapsed := float64(time.Since(start)) / float64(time.Second) if s.Root.Metrics != nil { @@ -6516,10 +6623,10 @@ func (s *TimerLayerReactionStore) DeleteAllWithEmojiName(emojiName string) error return err } -func (s *TimerLayerReactionStore) DeleteOrphanedRows(limit int) (int64, error) { +func (s *TimerLayerReactionStore) DeleteOrphanedRowsByIds(r *model.RetentionIdsForDeletion) error { start := time.Now() - result, err := s.ReactionStore.DeleteOrphanedRows(limit) + err := s.ReactionStore.DeleteOrphanedRowsByIds(r) elapsed := float64(time.Since(start)) / float64(time.Second) if s.Root.Metrics != nil { @@ -6527,9 +6634,9 @@ func (s *TimerLayerReactionStore) DeleteOrphanedRows(limit int) (int64, error) { if err == nil { success = "true" } - s.Root.Metrics.ObserveStoreMethodDuration("ReactionStore.DeleteOrphanedRows", success, elapsed) + s.Root.Metrics.ObserveStoreMethodDuration("ReactionStore.DeleteOrphanedRowsByIds", success, elapsed) } - return result, err + return err } func (s *TimerLayerReactionStore) GetForPost(postID string, allowFromCache bool) ([]*model.Reaction, error) { @@ -6580,6 +6687,22 @@ func (s *TimerLayerReactionStore) PermanentDeleteBatch(endTime int64, limit int6 return result, err } +func (s *TimerLayerReactionStore) PermanentDeleteByUser(userID string) error { + start := time.Now() + + err := s.ReactionStore.PermanentDeleteByUser(userID) + + elapsed := float64(time.Since(start)) / float64(time.Second) + if s.Root.Metrics != nil { + success := "false" + if err == nil { + success = "true" + } + s.Root.Metrics.ObserveStoreMethodDuration("ReactionStore.PermanentDeleteByUser", success, elapsed) + } + return err +} + func (s *TimerLayerReactionStore) Save(reaction *model.Reaction) (*model.Reaction, error) { start := time.Now() @@ -6884,6 +7007,22 @@ func (s *TimerLayerRetentionPolicyStore) GetCount() (int64, error) { return result, err } +func (s *TimerLayerRetentionPolicyStore) GetIdsForDeletionByTableName(tableName string, limit int) ([]*model.RetentionIdsForDeletion, error) { + start := time.Now() + + result, err := s.RetentionPolicyStore.GetIdsForDeletionByTableName(tableName, limit) + + elapsed := float64(time.Since(start)) / float64(time.Second) + if s.Root.Metrics != nil { + success := "false" + if err == nil { + success = "true" + } + s.Root.Metrics.ObserveStoreMethodDuration("RetentionPolicyStore.GetIdsForDeletionByTableName", success, elapsed) + } + return result, err +} + func (s *TimerLayerRetentionPolicyStore) GetTeamPoliciesCountForUser(userID string) (int64, error) { start := time.Now() @@ -8483,6 +8622,22 @@ func (s *TimerLayerTeamStore) GetChannelUnreadsForTeam(teamID string, userID str return result, err } +func (s *TimerLayerTeamStore) GetCommonTeamIDsForMultipleUsers(userIDs []string) ([]string, error) { + start := time.Now() + + result, err := s.TeamStore.GetCommonTeamIDsForMultipleUsers(userIDs) + + elapsed := float64(time.Since(start)) / float64(time.Second) + if s.Root.Metrics != nil { + success := "false" + if err == nil { + success = "true" + } + s.Root.Metrics.ObserveStoreMethodDuration("TeamStore.GetCommonTeamIDsForMultipleUsers", success, elapsed) + } + return result, err +} + func (s *TimerLayerTeamStore) GetCommonTeamIDsForTwoUsers(userID string, otherUserID string) ([]string, error) { start := time.Now() @@ -10016,6 +10171,22 @@ func (s *TimerLayerUserStore) GetByEmail(email string) (*model.User, error) { return result, err } +func (s *TimerLayerUserStore) GetByRemoteID(remoteID string) (*model.User, error) { + start := time.Now() + + result, err := s.UserStore.GetByRemoteID(remoteID) + + elapsed := float64(time.Since(start)) / float64(time.Second) + if s.Root.Metrics != nil { + success := "false" + if err == nil { + success = "true" + } + s.Root.Metrics.ObserveStoreMethodDuration("UserStore.GetByRemoteID", success, elapsed) + } + return result, err +} + func (s *TimerLayerUserStore) GetByUsername(username string) (*model.User, error) { start := time.Now() @@ -11565,6 +11736,7 @@ func New(childStore store.Store, metrics einterfaces.MetricsInterface) *TimerLay newStore.CommandStore = &TimerLayerCommandStore{CommandStore: childStore.Command(), Root: &newStore} newStore.CommandWebhookStore = &TimerLayerCommandWebhookStore{CommandWebhookStore: childStore.CommandWebhook(), Root: &newStore} newStore.ComplianceStore = &TimerLayerComplianceStore{ComplianceStore: childStore.Compliance(), Root: &newStore} + newStore.DesktopTokensStore = &TimerLayerDesktopTokensStore{DesktopTokensStore: childStore.DesktopTokens(), Root: &newStore} newStore.DraftStore = &TimerLayerDraftStore{DraftStore: childStore.Draft(), Root: &newStore} newStore.EmojiStore = &TimerLayerEmojiStore{EmojiStore: childStore.Emoji(), Root: &newStore} newStore.FileInfoStore = &TimerLayerFileInfoStore{FileInfoStore: childStore.FileInfo(), Root: &newStore} diff --git a/server/channels/utils/api.go b/server/channels/utils/api.go index af46f36aaa8..ca865ab61c7 100644 --- a/server/channels/utils/api.go +++ b/server/channels/utils/api.go @@ -16,6 +16,7 @@ import ( "github.com/mattermost/mattermost/server/public/model" "github.com/mattermost/mattermost/server/public/shared/i18n" + "github.com/mattermost/mattermost/server/public/utils" ) func CheckOrigin(r *http.Request, allowedOrigins string) bool { @@ -112,13 +113,8 @@ func RenderMobileAuthComplete(w http.ResponseWriter, redirectURL string) { func RenderMobileError(config *model.Config, w http.ResponseWriter, err *model.AppError, redirectURL string) { var link = template.HTMLEscapeString(redirectURL) - var invalidSchemes = map[string]bool{ - "data": true, - "javascript": true, - "vbscript": true, - } u, redirectErr := url.Parse(redirectURL) - if redirectErr != nil || invalidSchemes[u.Scheme] { + if redirectErr != nil || !utils.Contains(config.NativeAppSettings.AppCustomURLSchemes, u.Scheme) { link = *config.ServiceSettings.SiteURL } RenderMobileMessage(w, ` diff --git a/server/channels/utils/file.go b/server/channels/utils/file.go index 495a5543112..d59d2789558 100644 --- a/server/channels/utils/file.go +++ b/server/channels/utils/file.go @@ -8,7 +8,7 @@ import ( "io" ) -var SizeLimitExceeded = errors.New("Size limit exceeded") +var ErrSizeLimitExceeded = errors.New("size limit exceeded") type LimitedReaderWithError struct { limitedReader *io.LimitedReader @@ -23,7 +23,7 @@ func NewLimitedReaderWithError(reader io.Reader, maxBytes int64) *LimitedReaderW func (l *LimitedReaderWithError) Read(p []byte) (int, error) { n, err := l.limitedReader.Read(p) if l.limitedReader.N <= 0 && err == io.EOF { - return n, SizeLimitExceeded + return n, ErrSizeLimitExceeded } return n, err } diff --git a/server/channels/utils/file_test.go b/server/channels/utils/file_test.go index 9b5bccb0bf9..d811b02624f 100644 --- a/server/channels/utils/file_test.go +++ b/server/channels/utils/file_test.go @@ -51,7 +51,7 @@ func TestLimitedReaderWithError(t *testing.T) { buf := make([]byte, moreThanMaxBytes) _, err = io.ReadFull(lr, buf) require.Error(t, err) - require.Equal(t, SizeLimitExceeded, err) + require.Equal(t, ErrSizeLimitExceeded, err) }) t.Run("multiple small reads, total larger than max size", func(t *testing.T) { @@ -71,6 +71,6 @@ func TestLimitedReaderWithError(t *testing.T) { // lets do it again _, err = io.ReadFull(lr, buf) require.Error(t, err) - require.Equal(t, SizeLimitExceeded, err) + require.Equal(t, ErrSizeLimitExceeded, err) }) } diff --git a/server/channels/utils/humanize.go b/server/channels/utils/humanize.go new file mode 100644 index 00000000000..47636e1eeeb --- /dev/null +++ b/server/channels/utils/humanize.go @@ -0,0 +1,25 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package utils + +import ( + "strings" + + "github.com/mattermost/mattermost/server/public/shared/i18n" +) + +func JoinList(items []string) string { + if len(items) == 0 { + return "" + } else if len(items) == 1 { + return items[0] + } else { + return i18n.T( + "humanize.list_join", + map[string]any{ + "OtherItems": strings.Join(items[:len(items)-1], ", "), + "LastItem": items[len(items)-1], + }) + } +} diff --git a/server/channels/utils/merge.go b/server/channels/utils/merge.go index cdeaa6e908f..8071ec59ec3 100644 --- a/server/channels/utils/merge.go +++ b/server/channels/utils/merge.go @@ -36,31 +36,27 @@ type MergeConfig struct { // retTS := ret.(testStruct) // return &retTS, nil // } -func Merge(base any, patch any, mergeConfig *MergeConfig) (any, error) { - if reflect.TypeOf(base) != reflect.TypeOf(patch) { - return nil, fmt.Errorf( - "cannot merge different types. base type: %s, patch type: %s", - reflect.TypeOf(base), - reflect.TypeOf(patch), - ) - } - +func Merge[T any](base T, patch T, mergeConfig *MergeConfig) (T, error) { commonType := reflect.TypeOf(base) baseVal := reflect.ValueOf(base) patchVal := reflect.ValueOf(patch) - if commonType.Kind() == reflect.Ptr { - commonType = commonType.Elem() - baseVal = baseVal.Elem() - patchVal = patchVal.Elem() - } - ret := reflect.New(commonType) val, ok := merge(baseVal, patchVal, mergeConfig) if ok { ret.Elem().Set(val) } - return ret.Elem().Interface(), nil + + r, ok := ret.Elem().Interface().(T) + if !ok { + return r, fmt.Errorf( + "Unexpected type of return element, expected %s, is %s", + commonType, + reflect.TypeOf(r), + ) + } + + return r, nil } // merge recursively merges patch into base and returns the new struct, ptr, slice/map, or value diff --git a/server/channels/utils/merge_test.go b/server/channels/utils/merge_test.go index d900658bd6e..17b6ebe0e9c 100644 --- a/server/channels/utils/merge_test.go +++ b/server/channels/utils/merge_test.go @@ -1549,8 +1549,8 @@ func mergeSimple(base, patch simple) (*simple, error) { if err != nil { return nil, err } - retS := ret.(simple) - return &retS, nil + + return &ret, nil } func mergeEvenSimpler(base, patch evenSimpler) (*evenSimpler, error) { @@ -1558,8 +1558,8 @@ func mergeEvenSimpler(base, patch evenSimpler) (*evenSimpler, error) { if err != nil { return nil, err } - retTS := ret.(evenSimpler) - return &retTS, nil + + return &ret, nil } func mergeEvenSimplerWithConfig(base, patch evenSimpler, mergeConfig *utils.MergeConfig) (*evenSimpler, error) { @@ -1567,8 +1567,8 @@ func mergeEvenSimplerWithConfig(base, patch evenSimpler, mergeConfig *utils.Merg if err != nil { return nil, err } - retTS := ret.(evenSimpler) - return &retTS, nil + + return &ret, nil } func mergeSliceStruct(base, patch sliceStruct) (*sliceStruct, error) { @@ -1576,8 +1576,8 @@ func mergeSliceStruct(base, patch sliceStruct) (*sliceStruct, error) { if err != nil { return nil, err } - retTS := ret.(sliceStruct) - return &retTS, nil + + return &ret, nil } func mergeMapPtr(base, patch mapPtr) (*mapPtr, error) { @@ -1585,8 +1585,8 @@ func mergeMapPtr(base, patch mapPtr) (*mapPtr, error) { if err != nil { return nil, err } - retTS := ret.(mapPtr) - return &retTS, nil + + return &ret, nil } func mergeMapPtrState(base, patch mapPtrState) (*mapPtrState, error) { @@ -1594,8 +1594,8 @@ func mergeMapPtrState(base, patch mapPtrState) (*mapPtrState, error) { if err != nil { return nil, err } - retTS := ret.(mapPtrState) - return &retTS, nil + + return &ret, nil } func mergeMapPtrState2(base, patch mapPtrState2) (*mapPtrState2, error) { @@ -1603,8 +1603,8 @@ func mergeMapPtrState2(base, patch mapPtrState2) (*mapPtrState2, error) { if err != nil { return nil, err } - retTS := ret.(mapPtrState2) - return &retTS, nil + + return &ret, nil } func mergeTestStructs(base, patch testStruct) (*testStruct, error) { @@ -1612,71 +1612,36 @@ func mergeTestStructs(base, patch testStruct) (*testStruct, error) { if err != nil { return nil, err } - retTS := ret.(testStruct) - return &retTS, nil + + return &ret, nil } func mergeStringIntMap(base, patch map[string]int) (map[string]int, error) { - ret, err := utils.Merge(base, patch, nil) - if err != nil { - return nil, err - } - retTS := ret.(map[string]int) - return retTS, nil + return utils.Merge(base, patch, nil) } func mergeStringPtrIntMap(base, patch map[string]*int) (map[string]*int, error) { - ret, err := utils.Merge(base, patch, nil) - if err != nil { - return nil, err - } - retTS := ret.(map[string]*int) - return retTS, nil + return utils.Merge(base, patch, nil) } func mergeStringSliceIntMap(base, patch map[string][]int) (map[string][]int, error) { - ret, err := utils.Merge(base, patch, nil) - if err != nil { - return nil, err - } - retTS := ret.(map[string][]int) - return retTS, nil + return utils.Merge(base, patch, nil) } func mergeMapOfMap(base, patch map[string]map[string]*int) (map[string]map[string]*int, error) { - ret, err := utils.Merge(base, patch, nil) - if err != nil { - return nil, err - } - retTS := ret.(map[string]map[string]*int) - return retTS, nil + return utils.Merge(base, patch, nil) } func mergeInterfaceMap(base, patch map[string]any) (map[string]any, error) { - ret, err := utils.Merge(base, patch, nil) - if err != nil { - return nil, err - } - retTS := ret.(map[string]any) - return retTS, nil + return utils.Merge(base, patch, nil) } func mergeStringSlices(base, patch []string) ([]string, error) { - ret, err := utils.Merge(base, patch, nil) - if err != nil { - return nil, err - } - retTS := ret.([]string) - return retTS, nil + return utils.Merge(base, patch, nil) } func mergeTestStructsPtrs(base, patch *testStruct) (*testStruct, error) { - ret, err := utils.Merge(base, patch, nil) - if err != nil { - return nil, err - } - retTS := ret.(testStruct) - return &retTS, nil + return utils.Merge(base, patch, nil) } func newBool(b bool) *bool { return &b } diff --git a/server/channels/utils/utils.go b/server/channels/utils/utils.go index 4f257536aa4..ede1c4b2498 100644 --- a/server/channels/utils/utils.go +++ b/server/channels/utils/utils.go @@ -14,17 +14,9 @@ import ( "github.com/pkg/errors" "github.com/mattermost/mattermost/server/public/model" + pUtils "github.com/mattermost/mattermost/server/public/utils" ) -func StringInSlice(a string, slice []string) bool { - for _, b := range slice { - if b == a { - return true - } - } - return false -} - // RemoveStringFromSlice removes the first occurrence of a from slice. func RemoveStringFromSlice(a string, slice []string) []string { for i, str := range slice { @@ -40,7 +32,7 @@ func RemoveStringsFromSlice(slice []string, strings ...string) []string { newSlice := []string{} for _, item := range slice { - if !StringInSlice(item, strings) { + if !pUtils.Contains(strings, item) { newSlice = append(newSlice, item) } } @@ -216,6 +208,25 @@ func IsValidMobileAuthRedirectURL(config *model.Config, redirectURL string) bool return false } +// This will only return TRUE if the request comes from a mobile running the Mobile App. +// If the request comes from a mobile using the browser, it will return FALSE. +func IsMobileRequest(r *http.Request) bool { + userAgent := r.UserAgent() + if userAgent == "" { + return false + } + + // Check if the User-Agent contain keywords found in mobile devices running the mobile App + mobileKeywords := []string{"Mobile", "Android", "iOS", "iPhone", "iPad"} + for _, keyword := range mobileKeywords { + if strings.Contains(userAgent, keyword) { + return true + } + } + + return false +} + // RoundOffToZeroes converts all digits to 0 except the 1st one. // Special case: If there is only 1 digit, then returns 0. func RoundOffToZeroes(n float64) int64 { diff --git a/server/channels/utils/utils_test.go b/server/channels/utils/utils_test.go index 0c9784f8d6c..6a531d3a82d 100644 --- a/server/channels/utils/utils_test.go +++ b/server/channels/utils/utils_test.go @@ -5,6 +5,7 @@ package utils import ( "net/http" + "net/http/httptest" "testing" "github.com/stretchr/testify/assert" @@ -279,6 +280,32 @@ func TestRoundOffToZeroes(t *testing.T) { } } +func TestIsMobileRequest(t *testing.T) { + testCases := []struct { + userAgent string + expected bool + }{ + // Test cases with mobile devices + {"Mozilla/5.0 (iPhone; CPU iPhone OS 15_0 like Mac OS X)", true}, + {"Mozilla/5.0 (Android 12; Mobile)", true}, + {"Mozilla/5.0 (Linux; Android 12; Pixel 6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.9999.99 Mobile Safari/537.36", true}, + + // Test cases with NO mobile devices + {"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.45 Safari/537.36", false}, + {"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.45 Safari/537.36", false}, + } + + for _, tc := range testCases { + req := httptest.NewRequest(http.MethodGet, "/", nil) + req.Header.Set("User-Agent", tc.userAgent) + + result := IsMobileRequest(req) + if result != tc.expected { + t.Errorf("User-Agent: %s, expected: %v, got: %v", tc.userAgent, tc.expected, result) + } + } +} + func TestRoundOffToZeroesResolution(t *testing.T) { messageGranularity := 3 storageGranularity := 8 diff --git a/server/channels/web/context.go b/server/channels/web/context.go index 85f73a02fbd..215afb6a0c6 100644 --- a/server/channels/web/context.go +++ b/server/channels/web/context.go @@ -12,8 +12,8 @@ import ( "github.com/mattermost/mattermost/server/public/model" "github.com/mattermost/mattermost/server/public/shared/i18n" "github.com/mattermost/mattermost/server/public/shared/mlog" + "github.com/mattermost/mattermost/server/public/shared/request" "github.com/mattermost/mattermost/server/v8/channels/app" - "github.com/mattermost/mattermost/server/v8/channels/app/request" "github.com/mattermost/mattermost/server/v8/channels/audit" "github.com/mattermost/mattermost/server/v8/channels/utils" ) @@ -59,10 +59,11 @@ func (c *Context) MakeAuditRecord(event string, initialStatus string) *audit.Rec EventName: event, Status: initialStatus, Actor: audit.EventActor{ - UserId: c.AppContext.Session().UserId, - SessionId: c.AppContext.Session().Id, - Client: c.AppContext.UserAgent(), - IpAddress: c.AppContext.IPAddress(), + UserId: c.AppContext.Session().UserId, + SessionId: c.AppContext.Session().Id, + Client: c.AppContext.UserAgent(), + IpAddress: c.AppContext.IPAddress(), + XForwardedFor: c.AppContext.XForwardedFor(), }, Meta: map[string]interface{}{ audit.KeyAPIPath: c.AppContext.Path(), @@ -221,6 +222,10 @@ func (c *Context) SetInvalidParam(parameter string) { c.Err = NewInvalidParamError(parameter) } +func (c *Context) SetInvalidParamWithDetails(parameter string, details string) { + c.Err = NewInvalidParamDetailedError(parameter, details) +} + func (c *Context) SetInvalidParamWithErr(parameter string, err error) { c.Err = NewInvalidParamError(parameter).Wrap(err) } @@ -269,6 +274,10 @@ func (c *Context) HandleEtag(etag string, routeName string, w http.ResponseWrite return false } +func NewInvalidParamDetailedError(parameter string, details string) *model.AppError { + err := model.NewAppError("Context", "api.context.invalid_body_param.app_error", map[string]any{"Name": parameter}, details, http.StatusBadRequest) + return err +} func NewInvalidParamError(parameter string) *model.AppError { err := model.NewAppError("Context", "api.context.invalid_body_param.app_error", map[string]any{"Name": parameter}, "", http.StatusBadRequest) return err diff --git a/server/channels/web/handlers.go b/server/channels/web/handlers.go index c1c01c91185..05b0e879823 100644 --- a/server/channels/web/handlers.go +++ b/server/channels/web/handlers.go @@ -14,17 +14,18 @@ import ( "strings" "time" - "github.com/mattermost/gziphandler" "github.com/opentracing/opentracing-go" "github.com/opentracing/opentracing-go/ext" spanlog "github.com/opentracing/opentracing-go/log" + "github.com/mattermost/gziphandler" + "github.com/mattermost/mattermost/server/public/model" "github.com/mattermost/mattermost/server/public/shared/i18n" "github.com/mattermost/mattermost/server/public/shared/mlog" + "github.com/mattermost/mattermost/server/public/shared/request" "github.com/mattermost/mattermost/server/v8/channels/app" app_opentracing "github.com/mattermost/mattermost/server/v8/channels/app/opentracing" - "github.com/mattermost/mattermost/server/v8/channels/app/request" "github.com/mattermost/mattermost/server/v8/channels/store/opentracinglayer" "github.com/mattermost/mattermost/server/v8/channels/utils" "github.com/mattermost/mattermost/server/v8/platform/services/tracing" @@ -148,6 +149,11 @@ func (h Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { appInstance := app.New(app.ServerConnector(h.Srv.Channels())) + c := &Context{ + AppContext: &request.Context{}, + App: appInstance, + } + requestID := model.NewId() var statusCode string defer func() { @@ -156,6 +162,10 @@ func (h Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { mlog.String("url", r.URL.Path), mlog.String("request_id", requestID), } + // if there is a session then include the user_id + if c.AppContext.Session() != nil { + responseLogFields = append(responseLogFields, mlog.String("user_id", c.AppContext.Session().UserId)) + } // Websockets are returning status code 0 to requests after closing the socket if statusCode != "0" { responseLogFields = append(responseLogFields, mlog.String("status_code", statusCode)) @@ -163,19 +173,18 @@ func (h Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { mlog.Debug("Received HTTP request", responseLogFields...) }() - c := &Context{ - AppContext: &request.Context{}, - App: appInstance, - } - t, _ := i18n.GetTranslationsAndLocaleFromRequest(r) - c.AppContext.SetT(t) - c.AppContext.SetRequestId(requestID) - c.AppContext.SetIPAddress(utils.GetIPAddress(r, c.App.Config().ServiceSettings.TrustedProxyIPHeader)) - c.AppContext.SetUserAgent(r.UserAgent()) - c.AppContext.SetAcceptLanguage(r.Header.Get("Accept-Language")) - c.AppContext.SetPath(r.URL.Path) - c.AppContext.SetContext(context.Background()) + c.AppContext = request.NewContext( + context.Background(), + requestID, + utils.GetIPAddress(r, c.App.Config().ServiceSettings.TrustedProxyIPHeader), + r.Header.Get("X-Forwarded-For"), + r.URL.Path, + r.UserAgent(), + r.Header.Get("Accept-Language"), + t, + ) + c.Params = ParamsFromRequest(r) c.Logger = c.App.Log() diff --git a/server/channels/web/handlers_test.go b/server/channels/web/handlers_test.go index e8a2254b75e..cb9fa99eec2 100644 --- a/server/channels/web/handlers_test.go +++ b/server/channels/web/handlers_test.go @@ -558,7 +558,7 @@ func TestGenerateDevCSP(t *testing.T) { *cfg.ServiceSettings.DeveloperFlags = "" }) - logger := mlog.CreateConsoleTestLogger(false, mlog.LvlWarn) + logger := mlog.CreateConsoleTestLogger(t) buf := &mlog.Buffer{} require.NoError(t, mlog.AddWriterTarget(logger, buf, false, mlog.LvlWarn)) @@ -570,8 +570,6 @@ func TestGenerateDevCSP(t *testing.T) { generateDevCSP(*c) - require.NoError(t, logger.Shutdown()) - assert.Equal(t, "", buf.String()) }) } diff --git a/server/channels/web/oauth.go b/server/channels/web/oauth.go index cc60ebfced2..e80ab016c05 100644 --- a/server/channels/web/oauth.go +++ b/server/channels/web/oauth.go @@ -11,6 +11,7 @@ import ( "net/url" "path/filepath" "strings" + "time" "github.com/mattermost/mattermost/server/public/model" "github.com/mattermost/mattermost/server/public/shared/i18n" @@ -344,6 +345,34 @@ func completeOAuth(c *Context, w http.ResponseWriter, r *http.Request) { } else { // For web c.App.AttachSessionCookies(c.AppContext, w, r) } + + desktopToken := "" + if val, ok := props["desktop_token"]; ok { + desktopToken = val + } + + if desktopToken != "" { + serverToken, serverTokenErr := c.App.GenerateAndSaveDesktopToken(time.Now().Unix(), user) + if serverTokenErr != nil { + serverTokenErr.Translate(c.AppContext.T) + c.LogErrorByCode(serverTokenErr) + renderError(serverTokenErr) + return + } + + queryString := map[string]string{ + "client_token": desktopToken, + "server_token": *serverToken, + } + if val, ok := props["redirect_to"]; ok { + queryString["redirect_to"] = val + } + if strings.HasPrefix(desktopToken, "dev-") { + queryString["isDesktopDev"] = "true" + } + + redirectURL = utils.AppendQueryParamsToURL(c.GetSiteURLHeader()+"/login/desktop", queryString) + } } w.Header().Set("Content-Type", "text/html; charset=utf-8") @@ -358,6 +387,7 @@ func loginWithOAuth(c *Context, w http.ResponseWriter, r *http.Request) { loginHint := r.URL.Query().Get("login_hint") redirectURL := r.URL.Query().Get("redirect_to") + desktopToken := r.URL.Query().Get("desktop_token") if redirectURL != "" && !utils.IsValidWebAuthRedirectURL(c.App.Config(), redirectURL) { c.Err = model.NewAppError("loginWithOAuth", "api.invalid_redirect_url", nil, "", http.StatusBadRequest) @@ -370,7 +400,7 @@ func loginWithOAuth(c *Context, w http.ResponseWriter, r *http.Request) { return } - authURL, err := c.App.GetOAuthLoginEndpoint(w, r, c.Params.Service, teamId, model.OAuthActionLogin, redirectURL, loginHint, false) + authURL, err := c.App.GetOAuthLoginEndpoint(w, r, c.Params.Service, teamId, model.OAuthActionLogin, redirectURL, loginHint, false, desktopToken) if err != nil { c.Err = err return @@ -399,7 +429,7 @@ func mobileLoginWithOAuth(c *Context, w http.ResponseWriter, r *http.Request) { return } - authURL, err := c.App.GetOAuthLoginEndpoint(w, r, c.Params.Service, teamId, model.OAuthActionMobile, redirectURL, "", true) + authURL, err := c.App.GetOAuthLoginEndpoint(w, r, c.Params.Service, teamId, model.OAuthActionMobile, redirectURL, "", true, "") if err != nil { c.Err = err return @@ -427,7 +457,9 @@ func signupWithOAuth(c *Context, w http.ResponseWriter, r *http.Request) { return } - authURL, err := c.App.GetOAuthSignupEndpoint(w, r, c.Params.Service, teamId) + desktopToken := r.URL.Query().Get("desktop_token") + + authURL, err := c.App.GetOAuthSignupEndpoint(w, r, c.Params.Service, teamId, desktopToken) if err != nil { c.Err = err return diff --git a/server/channels/web/oauth_test.go b/server/channels/web/oauth_test.go index b90c8f6a44d..1d0cde142f9 100644 --- a/server/channels/web/oauth_test.go +++ b/server/channels/web/oauth_test.go @@ -402,7 +402,10 @@ func TestMobileLoginWithOAuth(t *testing.T) { } var siteURL = "http://localhost:8065" - th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.SiteURL = siteURL }) + th.App.UpdateConfig(func(cfg *model.Config) { + *cfg.ServiceSettings.SiteURL = siteURL + *cfg.GitLabSettings.Enable = true + }) translationFunc := i18n.GetUserTranslations("en") c.AppContext.SetT(translationFunc) @@ -410,28 +413,40 @@ func TestMobileLoginWithOAuth(t *testing.T) { provider := &MattermostTestProvider{} einterfaces.RegisterOAuthProvider(model.ServiceGitlab, provider) - t.Run("Should include redirect URL in the output when valid URL Scheme is passed", func(t *testing.T) { + t.Run("Should redirect to the SSO login page when valid URL Scheme is passed as redirect_to parameter", func(t *testing.T) { + responseWriter := httptest.NewRecorder() + request, _ := http.NewRequest(http.MethodGet, th.App.GetSiteURL()+"/oauth/gitlab/mobile_login?redirect_to="+url.QueryEscape("mmauth://"), nil) + mobileLoginWithOAuth(c, responseWriter, request) + assert.Equal(t, responseWriter.Code, 302) + assert.NotContains(t, responseWriter.Body.String(), siteURL) + }) + + t.Run("Should include SiteURL in the output when invalid URL Scheme is passed", func(t *testing.T) { + einterfaces.RegisterOAuthProvider(model.ServiceGitlab, provider) responseWriter := httptest.NewRecorder() request, _ := http.NewRequest(http.MethodGet, th.App.GetSiteURL()+"/oauth/gitlab/mobile_login?redirect_to="+url.QueryEscape("randomScheme://"), nil) mobileLoginWithOAuth(c, responseWriter, request) - assert.Contains(t, responseWriter.Body.String(), "randomScheme://") - assert.NotContains(t, responseWriter.Body.String(), siteURL) + body := responseWriter.Body.String() + assert.NotContains(t, body, "randomScheme://") + assert.Contains(t, body, siteURL) }) t.Run("Should not include the redirect URL consisting of javascript protocol", func(t *testing.T) { responseWriter := httptest.NewRecorder() request, _ := http.NewRequest(http.MethodGet, th.App.GetSiteURL()+"/oauth/gitlab/mobile_login?redirect_to="+url.QueryEscape("javascript:alert('hello')"), nil) mobileLoginWithOAuth(c, responseWriter, request) - assert.NotContains(t, responseWriter.Body.String(), "javascript:alert('hello')") - assert.Contains(t, responseWriter.Body.String(), siteURL) + body := responseWriter.Body.String() + assert.NotContains(t, body, "javascript:alert('hello')") + assert.Contains(t, body, siteURL) }) t.Run("Should not include the redirect URL consisting of javascript protocol in mixed case", func(t *testing.T) { responseWriter := httptest.NewRecorder() request, _ := http.NewRequest(http.MethodGet, th.App.GetSiteURL()+"/oauth/gitlab/mobile_login?redirect_to="+url.QueryEscape("JaVasCript:alert('hello')"), nil) mobileLoginWithOAuth(c, responseWriter, request) - assert.NotContains(t, responseWriter.Body.String(), "JaVasCript:alert('hello')") - assert.Contains(t, responseWriter.Body.String(), siteURL) + body := responseWriter.Body.String() + assert.NotContains(t, body, "JaVasCript:alert('hello')") + assert.Contains(t, body, siteURL) }) } @@ -608,8 +623,7 @@ func TestOAuthComplete_ErrorMessages(t *testing.T) { translationFunc := i18n.GetUserTranslations("en") c.AppContext.SetT(translationFunc) - c.Logger = mlog.CreateConsoleTestLogger(true, mlog.LvlDebug) - defer c.Logger.Shutdown() + c.Logger = mlog.CreateConsoleTestLogger(t) th.App.UpdateConfig(func(cfg *model.Config) { *cfg.GitLabSettings.Enable = true }) th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableOAuthServiceProvider = true }) provider := &MattermostTestProvider{} diff --git a/server/channels/web/params.go b/server/channels/web/params.go index 4de487ee9b2..a985d2dde94 100644 --- a/server/channels/web/params.go +++ b/server/channels/web/params.go @@ -75,6 +75,7 @@ type Params struct { NotAssociatedToChannel string Paginate *bool IncludeMemberCount bool + IncludeMemberIDs bool NotAssociatedToGroup string ExcludeDefaultChannels bool LimitAfter int @@ -83,6 +84,7 @@ type Params struct { IncludeTotalCount bool IncludeDeleted bool FilterAllowReference bool + FilterArchived bool FilterParentTeamPermitted bool CategoryId string WarnMetricId string @@ -208,6 +210,7 @@ func ParamsFromRequest(r *http.Request) *Params { params.NotAssociatedToTeam = query.Get("not_associated_to_team") params.NotAssociatedToChannel = query.Get("not_associated_to_channel") params.FilterAllowReference, _ = strconv.ParseBool(query.Get("filter_allow_reference")) + params.FilterArchived, _ = strconv.ParseBool(query.Get("filter_archived")) params.FilterParentTeamPermitted, _ = strconv.ParseBool(query.Get("filter_parent_team_permitted")) params.IncludeChannelMemberCount = query.Get("include_channel_member_count") @@ -216,6 +219,7 @@ func ParamsFromRequest(r *http.Request) *Params { } params.IncludeMemberCount, _ = strconv.ParseBool(query.Get("include_member_count")) + params.IncludeMemberIDs, _ = strconv.ParseBool(query.Get("include_member_ids")) params.NotAssociatedToGroup = query.Get("not_associated_to_group") params.ExcludeDefaultChannels, _ = strconv.ParseBool(query.Get("exclude_default_channels")) params.GroupIDs = query.Get("group_ids") diff --git a/server/channels/web/saml.go b/server/channels/web/saml.go index e72b0c8bf32..3d9c8b65e23 100644 --- a/server/channels/web/saml.go +++ b/server/channels/web/saml.go @@ -9,6 +9,7 @@ import ( "net/http" "strconv" "strings" + "time" "github.com/mattermost/mattermost/server/public/model" "github.com/mattermost/mattermost/server/public/shared/mlog" @@ -59,6 +60,11 @@ func loginWithSaml(c *Context, w http.ResponseWriter, r *http.Request) { relayProps["redirect_to"] = redirectURL } + desktopToken := r.URL.Query().Get("desktop_token") + if desktopToken != "" { + relayProps["desktop_token"] = desktopToken + } + relayProps[model.UserAuthServiceIsMobile] = strconv.FormatBool(isMobile) if len(relayProps) > 0 { @@ -185,6 +191,30 @@ func completeSaml(c *Context, w http.ResponseWriter, r *http.Request) { c.App.AttachSessionCookies(c.AppContext, w, r) + desktopToken := relayProps["desktop_token"] + if desktopToken != "" { + serverToken, serverTokenErr := c.App.GenerateAndSaveDesktopToken(time.Now().Unix(), user) + if serverTokenErr != nil { + handleError(serverTokenErr) + return + } + + queryString := map[string]string{ + "client_token": desktopToken, + "server_token": *serverToken, + } + if val, ok := relayProps["redirect_to"]; ok { + queryString["redirect_to"] = val + } + if strings.HasPrefix(desktopToken, "dev-") { + queryString["isDesktopDev"] = "true" + } + + redirectURL = utils.AppendQueryParamsToURL(c.GetSiteURLHeader()+"/login/desktop", queryString) + http.Redirect(w, r, redirectURL, http.StatusFound) + return + } + if hasRedirectURL { if isMobile { // Mobile clients with redirect url support diff --git a/server/channels/web/unsupported_browser.go b/server/channels/web/unsupported_browser.go index 692847fded4..f30fbef9d7a 100644 --- a/server/channels/web/unsupported_browser.go +++ b/server/channels/web/unsupported_browser.go @@ -8,7 +8,7 @@ import ( "github.com/avct/uasurfer" - "github.com/mattermost/mattermost/server/v8/channels/app/request" + "github.com/mattermost/mattermost/server/public/shared/request" "github.com/mattermost/mattermost/server/v8/platform/shared/templates" ) diff --git a/server/channels/web/web_test.go b/server/channels/web/web_test.go index 11ebf928856..70df148dbd5 100644 --- a/server/channels/web/web_test.go +++ b/server/channels/web/web_test.go @@ -21,8 +21,8 @@ import ( "github.com/mattermost/mattermost/server/public/plugin" "github.com/mattermost/mattermost/server/public/plugin/utils" "github.com/mattermost/mattermost/server/public/shared/mlog" + "github.com/mattermost/mattermost/server/public/shared/request" "github.com/mattermost/mattermost/server/v8/channels/app" - "github.com/mattermost/mattermost/server/v8/channels/app/request" "github.com/mattermost/mattermost/server/v8/channels/store/localcachelayer" "github.com/mattermost/mattermost/server/v8/channels/store/storetest/mocks" "github.com/mattermost/mattermost/server/v8/config" diff --git a/server/channels/wsapi/user.go b/server/channels/wsapi/user.go index b407025bcd3..d94db4fe846 100644 --- a/server/channels/wsapi/user.go +++ b/server/channels/wsapi/user.go @@ -5,7 +5,7 @@ package wsapi import ( "github.com/mattermost/mattermost/server/public/model" - "github.com/mattermost/mattermost/server/v8/channels/app/request" + "github.com/mattermost/mattermost/server/public/shared/request" ) func (api *API) InitUser() { diff --git a/server/cmd/mattermost/commands/export.go b/server/cmd/mattermost/commands/export.go index 8278d6b4a89..816e091c879 100644 --- a/server/cmd/mattermost/commands/export.go +++ b/server/cmd/mattermost/commands/export.go @@ -11,8 +11,8 @@ import ( "time" "github.com/mattermost/mattermost/server/public/model" + "github.com/mattermost/mattermost/server/public/shared/request" "github.com/mattermost/mattermost/server/v8/channels/app" - "github.com/mattermost/mattermost/server/v8/channels/app/request" "github.com/mattermost/mattermost/server/v8/channels/audit" "github.com/pkg/errors" @@ -81,6 +81,7 @@ func init() { GlobalRelayZipExportCmd.Flags().Int("limit", -1, "The number of posts to export. The default of -1 means no limit.") BulkExportCmd.Flags().Bool("all-teams", true, "Export all teams from the server.") + BulkExportCmd.Flags().Bool("with-archived-channels", false, "Also exports archived channels.") BulkExportCmd.Flags().Bool("attachments", false, "Also export file attachments.") BulkExportCmd.Flags().Bool("archive", false, "Outputs a single archive file.") @@ -137,7 +138,10 @@ func scheduleExportCmdF(command *cobra.Command, args []string) error { defer cancel() } - job, err := messageExportI.StartSynchronizeJob(ctx, startTime) + c := request.EmptyContext(a.Log()) + c.SetContext(ctx) + + job, err := messageExportI.StartSynchronizeJob(c, startTime) if err != nil || job.Status == model.JobStatusError || job.Status == model.JobStatusCanceled { CommandPrintErrorln("ERROR: Message export job failed. Please check the server logs") } else { @@ -226,6 +230,11 @@ func bulkExportCmdF(command *cobra.Command, args []string) error { return errors.Wrap(err, "archive flag error") } + withArchivedChannels, err := command.Flags().GetBool("with-archived-channels") + if err != nil { + return errors.Wrap(err, "with-archived-channels flag error") + } + fileWriter, err := os.Create(args[0]) if err != nil { return err @@ -240,6 +249,7 @@ func bulkExportCmdF(command *cobra.Command, args []string) error { var opts model.BulkExportOpts opts.IncludeAttachments = attachments opts.CreateArchive = archive + opts.IncludeArchivedChannels = withArchivedChannels if err := a.BulkExport(request.EmptyContext(a.Log()), fileWriter, filepath.Dir(outPath), nil /* nil job since it's spawned from CLI */, opts); err != nil { CommandPrintErrorln(err.Error()) return err diff --git a/server/cmd/mattermost/commands/import.go b/server/cmd/mattermost/commands/import.go index 2c62f432d97..3b5ca0145d3 100644 --- a/server/cmd/mattermost/commands/import.go +++ b/server/cmd/mattermost/commands/import.go @@ -11,8 +11,8 @@ import ( "github.com/spf13/cobra" "github.com/mattermost/mattermost/server/public/model" + "github.com/mattermost/mattermost/server/public/shared/request" "github.com/mattermost/mattermost/server/v8/channels/app" - "github.com/mattermost/mattermost/server/v8/channels/app/request" "github.com/mattermost/mattermost/server/v8/channels/audit" ) diff --git a/server/cmd/mattermost/commands/init.go b/server/cmd/mattermost/commands/init.go index fc1e07b6a16..77f40c63868 100644 --- a/server/cmd/mattermost/commands/init.go +++ b/server/cmd/mattermost/commands/init.go @@ -9,8 +9,8 @@ import ( "github.com/mattermost/mattermost/server/public/model" "github.com/mattermost/mattermost/server/public/shared/i18n" + "github.com/mattermost/mattermost/server/public/shared/request" "github.com/mattermost/mattermost/server/v8/channels/app" - "github.com/mattermost/mattermost/server/v8/channels/app/request" "github.com/mattermost/mattermost/server/v8/channels/store" "github.com/mattermost/mattermost/server/v8/channels/store/sqlstore" "github.com/mattermost/mattermost/server/v8/channels/utils" diff --git a/server/cmd/mmctl/commands/enterprise.go b/server/cmd/mmctl/commands/enterprise.go deleted file mode 100644 index 96cdea220d1..00000000000 --- a/server/cmd/mmctl/commands/enterprise.go +++ /dev/null @@ -1,24 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -//go:build enterprise -// +build enterprise - -package commands - -import ( - // Enterprise Deps - _ "github.com/gorilla/handlers" - _ "github.com/hako/durafmt" - _ "github.com/hashicorp/memberlist" - _ "github.com/mattermost/gosaml2" - _ "github.com/mattermost/ldap" - _ "github.com/mattermost/mattermost/server/v8/channels/imports" - _ "github.com/mattermost/mattermost/server/v8/channels/utils/testutils" - _ "github.com/mattermost/rsc/qr" - _ "github.com/prometheus/client_golang/prometheus" - _ "github.com/prometheus/client_golang/prometheus/collectors" - _ "github.com/prometheus/client_golang/prometheus/promhttp" - _ "github.com/tylerb/graceful" - _ "gopkg.in/olivere/elastic.v6" -) diff --git a/server/cmd/mmctl/commands/export.go b/server/cmd/mmctl/commands/export.go index ccb4f5ba2de..6ee4443fd61 100644 --- a/server/cmd/mmctl/commands/export.go +++ b/server/cmd/mmctl/commands/export.go @@ -100,6 +100,7 @@ func init() { _ = ExportCreateCmd.Flags().MarkDeprecated("attachments", "the tool now includes attachments by default. The flag will be removed in a future version.") ExportCreateCmd.Flags().Bool("no-attachments", false, "Set to true to exclude file attachments in the export file.") + ExportCreateCmd.Flags().Bool("include-archived-channels", false, "Set to true to include archived channels in the export file.") ExportDownloadCmd.Flags().Bool("resume", false, "Set to true to resume an export download.") _ = ExportDownloadCmd.Flags().MarkHidden("resume") @@ -136,6 +137,11 @@ func exportCreateCmdF(c client.Client, command *cobra.Command, args []string) er data["include_attachments"] = "true" } + includeArchivedChannels, _ := command.Flags().GetBool("include-archived-channels") + if includeArchivedChannels { + data["include_archived_channels"] = "true" + } + job, _, err := c.CreateJob(context.TODO(), &model.Job{ Type: model.JobTypeExportProcess, Data: data, diff --git a/server/cmd/mmctl/commands/export_e2e_test.go b/server/cmd/mmctl/commands/export_e2e_test.go index 55ea15c95af..4257ecc04d2 100644 --- a/server/cmd/mmctl/commands/export_e2e_test.go +++ b/server/cmd/mmctl/commands/export_e2e_test.go @@ -10,6 +10,7 @@ import ( "path/filepath" "time" + "github.com/mattermost/mattermost/server/public/shared/request" "github.com/mattermost/mattermost/server/v8/cmd/mmctl/client" "github.com/mattermost/mattermost/server/v8/cmd/mmctl/printer" @@ -275,16 +276,20 @@ func (s *MmctlE2ETestSuite) TestExportDownloadCmdF() { func (s *MmctlE2ETestSuite) TestExportJobShowCmdF() { s.SetupTestHelper().InitBasic() + ctx := request.EmptyContext(s.th.App.Log()) - job, appErr := s.th.App.CreateJob(&model.Job{ + job, appErr := s.th.App.CreateJob(ctx, &model.Job{ Type: model.JobTypeExportProcess, }) s.Require().Nil(appErr) + job.Logger = nil + + time.Sleep(time.Millisecond) s.Run("MM-T3885 - no permissions", func() { printer.Clean() - job1, appErr := s.th.App.CreateJob(&model.Job{ + job1, appErr := s.th.App.CreateJob(ctx, &model.Job{ Type: model.JobTypeExportProcess, }) s.Require().Nil(appErr) @@ -317,6 +322,7 @@ func (s *MmctlE2ETestSuite) TestExportJobShowCmdF() { func (s *MmctlE2ETestSuite) TestExportJobListCmdF() { s.SetupTestHelper().InitBasic() + ctx := request.EmptyContext(s.th.App.Log()) s.Run("MM-T3887 - no permissions", func() { printer.Clean() @@ -356,24 +362,26 @@ func (s *MmctlE2ETestSuite) TestExportJobListCmdF() { cmd.Flags().Int("per-page", perPage, "") cmd.Flags().Bool("all", false, "") - _, appErr := s.th.App.CreateJob(&model.Job{ + _, appErr := s.th.App.CreateJob(ctx, &model.Job{ Type: model.JobTypeExportProcess, }) s.Require().Nil(appErr) time.Sleep(time.Millisecond) - job2, appErr := s.th.App.CreateJob(&model.Job{ + job2, appErr := s.th.App.CreateJob(ctx, &model.Job{ Type: model.JobTypeExportProcess, }) s.Require().Nil(appErr) + job2.Logger = nil time.Sleep(time.Millisecond) - job3, appErr := s.th.App.CreateJob(&model.Job{ + job3, appErr := s.th.App.CreateJob(ctx, &model.Job{ Type: model.JobTypeExportProcess, }) s.Require().Nil(appErr) + job3.Logger = nil err := exportJobListCmdF(c, cmd, nil) s.Require().Nil(err) @@ -386,13 +394,14 @@ func (s *MmctlE2ETestSuite) TestExportJobListCmdF() { func (s *MmctlE2ETestSuite) TestExportJobCancelCmdF() { s.SetupTestHelper().InitBasic() + ctx := request.EmptyContext(s.th.App.Log()) s.Run("Cancel an export job without permissions", func() { printer.Clean() cmd := &cobra.Command{} - job, appErr := s.th.App.CreateJob(&model.Job{ + job, appErr := s.th.App.CreateJob(ctx, &model.Job{ Type: model.JobTypeExportProcess, }) s.Require().Nil(appErr) @@ -421,14 +430,14 @@ func (s *MmctlE2ETestSuite) TestExportJobCancelCmdF() { cmd := &cobra.Command{} - job1, appErr := s.th.App.CreateJob(&model.Job{ + job1, appErr := s.th.App.CreateJob(ctx, &model.Job{ Type: model.JobTypeExportProcess, }) s.Require().Nil(appErr) time.Sleep(time.Millisecond) - job2, appErr := s.th.App.CreateJob(&model.Job{ + job2, appErr := s.th.App.CreateJob(ctx, &model.Job{ Type: model.JobTypeExportProcess, }) s.Require().Nil(appErr) @@ -439,11 +448,11 @@ func (s *MmctlE2ETestSuite) TestExportJobCancelCmdF() { s.Require().Empty(printer.GetErrorLines()) // Get job1 again to refresh its status - job1, appErr = s.th.App.GetJob(job1.Id) + job1, appErr = s.th.App.GetJob(ctx, job1.Id) s.Require().Nil(appErr) // Get job2 again to ensure its status did not change - job2, _ = s.th.App.GetJob(job2.Id) + job2, _ = s.th.App.GetJob(ctx, job2.Id) s.Require().Nil(appErr) s.Require().Equal(job1.Status, model.JobStatusCanceled) diff --git a/server/cmd/mmctl/commands/extract_e2e_test.go b/server/cmd/mmctl/commands/extract_e2e_test.go index 65da74eabcb..2b69b16a4eb 100644 --- a/server/cmd/mmctl/commands/extract_e2e_test.go +++ b/server/cmd/mmctl/commands/extract_e2e_test.go @@ -9,6 +9,7 @@ import ( "path/filepath" "time" + "github.com/mattermost/mattermost/server/public/shared/request" "github.com/mattermost/mattermost/server/v8/cmd/mmctl/client" "github.com/mattermost/mattermost/server/v8/cmd/mmctl/printer" @@ -70,17 +71,19 @@ func (s *MmctlE2ETestSuite) TestExtractRunCmdF() { func (s *MmctlE2ETestSuite) TestExtractJobShowCmdF() { s.SetupTestHelper().InitBasic() + ctx := request.EmptyContext(s.th.App.Log()) - job, appErr := s.th.App.CreateJob(&model.Job{ + job, appErr := s.th.App.CreateJob(ctx, &model.Job{ Type: model.JobTypeExtractContent, Data: map[string]string{}, }) s.Require().Nil(appErr) + job.Logger = nil s.Run("no permissions", func() { printer.Clean() - job1, appErr := s.th.App.CreateJob(&model.Job{ + job1, appErr := s.th.App.CreateJob(ctx, &model.Job{ Type: model.JobTypeExtractContent, Data: map[string]string{}, }) @@ -116,6 +119,7 @@ func (s *MmctlE2ETestSuite) TestExtractJobShowCmdF() { func (s *MmctlE2ETestSuite) TestExtractJobListCmdF() { s.SetupTestHelper().InitBasic() + ctx := request.EmptyContext(s.th.App.Log()) s.Run("no permissions", func() { printer.Clean() @@ -156,7 +160,7 @@ func (s *MmctlE2ETestSuite) TestExtractJobListCmdF() { cmd.Flags().Int("per-page", perPage, "") cmd.Flags().Bool("all", false, "") - _, appErr := s.th.App.CreateJob(&model.Job{ + _, appErr := s.th.App.CreateJob(ctx, &model.Job{ Type: model.JobTypeExtractContent, Data: map[string]string{}, }) @@ -164,19 +168,23 @@ func (s *MmctlE2ETestSuite) TestExtractJobListCmdF() { time.Sleep(time.Millisecond) - job2, appErr := s.th.App.CreateJob(&model.Job{ + job2, appErr := s.th.App.CreateJob(ctx, &model.Job{ Type: model.JobTypeExtractContent, Data: map[string]string{}, }) s.Require().Nil(appErr) + job2.Logger = nil time.Sleep(time.Millisecond) - job3, appErr := s.th.App.CreateJob(&model.Job{ + job3, appErr := s.th.App.CreateJob(ctx, &model.Job{ Type: model.JobTypeExtractContent, Data: map[string]string{}, }) s.Require().Nil(appErr) + job3.Logger = nil + + time.Sleep(time.Millisecond) err := extractJobListCmdF(c, cmd, nil) s.Require().Nil(err) diff --git a/server/cmd/mmctl/commands/import_e2e_test.go b/server/cmd/mmctl/commands/import_e2e_test.go index 7eff0621f38..428e1f645ca 100644 --- a/server/cmd/mmctl/commands/import_e2e_test.go +++ b/server/cmd/mmctl/commands/import_e2e_test.go @@ -9,6 +9,7 @@ import ( "path/filepath" "time" + "github.com/mattermost/mattermost/server/public/shared/request" "github.com/mattermost/mattermost/server/v8/cmd/mmctl/client" "github.com/mattermost/mattermost/server/v8/cmd/mmctl/printer" @@ -250,17 +251,19 @@ func (s *MmctlE2ETestSuite) TestImportListIncompleteCmdF() { func (s *MmctlE2ETestSuite) TestImportJobShowCmdF() { s.SetupTestHelper().InitBasic() + ctx := request.EmptyContext(s.th.App.Log()) - job, appErr := s.th.App.CreateJob(&model.Job{ + job, appErr := s.th.App.CreateJob(ctx, &model.Job{ Type: model.JobTypeImportProcess, Data: map[string]string{"import_file": "import1.zip"}, }) s.Require().Nil(appErr) + job.Logger = nil s.Run("no permissions", func() { printer.Clean() - job1, appErr := s.th.App.CreateJob(&model.Job{ + job1, appErr := s.th.App.CreateJob(ctx, &model.Job{ Type: model.JobTypeImportProcess, Data: map[string]string{"import_file": "import1.zip"}, }) @@ -296,6 +299,7 @@ func (s *MmctlE2ETestSuite) TestImportJobShowCmdF() { func (s *MmctlE2ETestSuite) TestImportJobListCmdF() { s.SetupTestHelper().InitBasic() + ctx := request.EmptyContext(s.th.App.Log()) s.Run("no permissions", func() { printer.Clean() @@ -336,7 +340,7 @@ func (s *MmctlE2ETestSuite) TestImportJobListCmdF() { cmd.Flags().Int("per-page", perPage, "") cmd.Flags().Bool("all", false, "") - _, appErr := s.th.App.CreateJob(&model.Job{ + _, appErr := s.th.App.CreateJob(ctx, &model.Job{ Type: model.JobTypeImportProcess, Data: map[string]string{"import_file": "import1.zip"}, }) @@ -344,19 +348,21 @@ func (s *MmctlE2ETestSuite) TestImportJobListCmdF() { time.Sleep(time.Millisecond) - job2, appErr := s.th.App.CreateJob(&model.Job{ + job2, appErr := s.th.App.CreateJob(ctx, &model.Job{ Type: model.JobTypeImportProcess, Data: map[string]string{"import_file": "import2.zip"}, }) s.Require().Nil(appErr) + job2.Logger = nil time.Sleep(time.Millisecond) - job3, appErr := s.th.App.CreateJob(&model.Job{ + job3, appErr := s.th.App.CreateJob(ctx, &model.Job{ Type: model.JobTypeImportProcess, Data: map[string]string{"import_file": "import3.zip"}, }) s.Require().Nil(appErr) + job3.Logger = nil err := importJobListCmdF(c, cmd, nil) s.Require().Nil(err) diff --git a/server/cmd/mmctl/commands/ldap_e2e_test.go b/server/cmd/mmctl/commands/ldap_e2e_test.go index deccccd633b..1c91338db7e 100644 --- a/server/cmd/mmctl/commands/ldap_e2e_test.go +++ b/server/cmd/mmctl/commands/ldap_e2e_test.go @@ -8,6 +8,7 @@ import ( "time" "github.com/mattermost/mattermost/server/public/model" + "github.com/mattermost/mattermost/server/public/shared/request" "github.com/mattermost/mattermost/server/v8/channels/api4" "github.com/mattermost/mattermost/server/v8/channels/utils/testutils" "github.com/spf13/cobra" @@ -51,6 +52,7 @@ func configForLdap(th *api4.TestHelper) { func (s *MmctlE2ETestSuite) TestLdapSyncCmd() { s.SetupEnterpriseTestHelper().InitBasic() configForLdap(s.th) + ctx := request.EmptyContext(s.th.App.Log()) s.Run("MM-T3971 Should not allow regular user to sync LDAP groups", func() { printer.Clean() @@ -64,7 +66,7 @@ func (s *MmctlE2ETestSuite) TestLdapSyncCmd() { s.RunForSystemAdminAndLocal("MM-T2529 Should sync LDAP groups", func(c client.Client) { printer.Clean() - jobs, appErr := s.th.App.GetJobsByTypePage(model.JobTypeLdapSync, 0, 100) + jobs, appErr := s.th.App.GetJobsByTypePage(ctx, model.JobTypeLdapSync, 0, 100) s.Require().Nil(appErr) initialNumJobs := len(jobs) @@ -78,7 +80,7 @@ func (s *MmctlE2ETestSuite) TestLdapSyncCmd() { // we need to wait a bit for job creation time.Sleep(time.Second) - jobs, appErr = s.th.App.GetJobsByTypePage(model.JobTypeLdapSync, 0, 100) + jobs, appErr = s.th.App.GetJobsByTypePage(ctx, model.JobTypeLdapSync, 0, 100) s.Require().Nil(appErr) s.Require().NotEmpty(jobs) s.Assert().Equal(initialNumJobs+1, len(jobs)) diff --git a/server/cmd/mmctl/commands/root.go b/server/cmd/mmctl/commands/root.go index 9d115900951..32ce53307f5 100644 --- a/server/cmd/mmctl/commands/root.go +++ b/server/cmd/mmctl/commands/root.go @@ -73,6 +73,9 @@ var RootCmd = &cobra.Command{ Long: `Mattermost offers workplace messaging across web, PC and phones with archiving, search and integration with your existing systems. Documentation available at https://docs.mattermost.com`, DisableAutoGenTag: true, PersistentPreRun: func(cmd *cobra.Command, args []string) { + for i, arg := range args { + args[i] = strings.TrimSpace(arg) + } format := viper.GetString("format") if viper.GetBool("disable-pager") { printer.OverrideEnablePager(false) diff --git a/server/cmd/mmctl/commands/root_test.go b/server/cmd/mmctl/commands/root_test.go new file mode 100644 index 00000000000..903bfbfd9d1 --- /dev/null +++ b/server/cmd/mmctl/commands/root_test.go @@ -0,0 +1,48 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package commands + +import ( + "bytes" + "fmt" + "strings" + + "github.com/spf13/cobra" +) + +func executeRawCommand(root *cobra.Command, args string) (c *cobra.Command, output string, err error) { + + actual := new(bytes.Buffer) + RootCmd.SetOut(actual) + RootCmd.SetErr(actual) + RootCmd.SetArgs(strings.Split(args, " ")) + c, err = RootCmd.ExecuteC() + return c, actual.String(), err +} + +func (s *MmctlUnitTestSuite) TestArgumentsHaveWhitespaceTrimmed() { + + arguments := []string{"value_1", "value_2"} + lineEndings := []string{"\n", "\r", "\r\n"} + prettyNames := []string{"-n", "-r", "-r-n"} + commandCalled := false + + for i, lineEnding := range lineEndings { + testName := fmt.Sprintf("Commands have their arguments stripped of whitespace[%s]", prettyNames[i]) + s.Run(testName, func() { + commandCalled = false + commandFunction := func(command *cobra.Command, args []string) { + commandCalled = true + s.Equal(arguments, args, "Expected arguments to have their whitespace trimmed") + + } + mockCommand := &cobra.Command{Use: "test", Run: commandFunction} + commandString := strings.Join([]string{"test", " ", arguments[0], lineEnding, " ", arguments[1], lineEnding}, "") + RootCmd.AddCommand(mockCommand) + executeRawCommand(RootCmd, commandString) + s.Require().True(commandCalled, "Expected mock command to be called") + }) + } + +} diff --git a/server/cmd/mmctl/commands/sampledata.go b/server/cmd/mmctl/commands/sampledata.go index 0b1e0c5315a..2d53aca1900 100644 --- a/server/cmd/mmctl/commands/sampledata.go +++ b/server/cmd/mmctl/commands/sampledata.go @@ -19,7 +19,6 @@ import ( "github.com/mattermost/mattermost/server/v8/cmd/mmctl/printer" "github.com/mattermost/mattermost/server/v8/channels/app/imports" - "github.com/mattermost/mattermost/server/v8/channels/utils" "github.com/mattermost/mattermost/server/public/model" pUtils "github.com/mattermost/mattermost/server/public/utils" @@ -362,7 +361,7 @@ func sampledataCmdF(c client.Client, command *cobra.Command, args []string) erro totalUsers := 3 + rand.Intn(3) for len(users) < totalUsers { user := allUsers[rand.Intn(len(allUsers))] - if !utils.StringInSlice(user, users) { + if !pUtils.Contains(users, user) { users = append(users, user) } } @@ -377,7 +376,7 @@ func sampledataCmdF(c client.Client, command *cobra.Command, args []string) erro totalUsers := 3 + rand.Intn(3) for len(users) < totalUsers { user := allUsers[rand.Intn(len(allUsers))] - if !utils.StringInSlice(user, users) { + if !pUtils.Contains(users, user) { users = append(users, user) } } diff --git a/server/cmd/mmctl/commands/utils_unix_test.go b/server/cmd/mmctl/commands/utils_unix_test.go index 9ba03b6f9db..8b98ad75318 100644 --- a/server/cmd/mmctl/commands/utils_unix_test.go +++ b/server/cmd/mmctl/commands/utils_unix_test.go @@ -13,6 +13,7 @@ import ( ) func TestCheckValidSocket(t *testing.T) { + t.Skip("https://mattermost.atlassian.net/browse/MM-54264") t.Run("should return error if the file is not a socket", func(t *testing.T) { f, err := ioutil.TempFile(os.TempDir(), "mmctl_socket_") require.NoError(t, err) diff --git a/server/cmd/mmctl/docs/mmctl_export_create.rst b/server/cmd/mmctl/docs/mmctl_export_create.rst index 3cb45e533ca..12de4fda9ca 100644 --- a/server/cmd/mmctl/docs/mmctl_export_create.rst +++ b/server/cmd/mmctl/docs/mmctl_export_create.rst @@ -20,8 +20,9 @@ Options :: - -h, --help help for create - --no-attachments Set to true to exclude file attachments in the export file. + -h, --help help for create + --include-archived-channels Set to true to include archived channels in the export file. + --no-attachments Set to true to exclude file attachments in the export file. Options inherited from parent commands ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/server/config/client.go b/server/config/client.go index 50f9c048a31..784ec5d5ebb 100644 --- a/server/config/client.go +++ b/server/config/client.go @@ -23,6 +23,7 @@ func GenerateClientConfig(c *model.Config, telemetryID string, license *model.Li props["LockTeammateNameDisplay"] = strconv.FormatBool(*c.TeamSettings.LockTeammateNameDisplay) props["ExperimentalPrimaryTeam"] = *c.TeamSettings.ExperimentalPrimaryTeam props["ExperimentalViewArchivedChannels"] = strconv.FormatBool(*c.TeamSettings.ExperimentalViewArchivedChannels) + props["EnableJoinLeaveMessageByDefault"] = strconv.FormatBool(*c.TeamSettings.EnableJoinLeaveMessageByDefault) props["EnableBotAccountCreation"] = strconv.FormatBool(*c.ServiceSettings.EnableBotAccountCreation) props["EnableOAuthServiceProvider"] = strconv.FormatBool(*c.ServiceSettings.EnableOAuthServiceProvider) @@ -78,8 +79,7 @@ func GenerateClientConfig(c *model.Config, telemetryID string, license *model.Li props["EnableEmojiPicker"] = strconv.FormatBool(*c.ServiceSettings.EnableEmojiPicker) props["EnableGifPicker"] = strconv.FormatBool(*c.ServiceSettings.EnableGifPicker) - props["GfycatApiKey"] = *c.ServiceSettings.GfycatAPIKey - props["GfycatApiSecret"] = *c.ServiceSettings.GfycatAPISecret + props["GiphySdkKey"] = getGiphySdkKey(c.ServiceSettings) props["MaxFileSize"] = strconv.FormatInt(*c.FileSettings.MaxFileSize, 10) props["MaxNotificationsPerChannel"] = strconv.FormatInt(*c.TeamSettings.MaxNotificationsPerChannel, 10) @@ -126,10 +126,9 @@ func GenerateClientConfig(c *model.Config, telemetryID string, license *model.Li props["DataRetentionMessageRetentionDays"] = "0" props["DataRetentionEnableFileDeletion"] = "false" props["DataRetentionFileRetentionDays"] = "0" - props["DataRetentionEnableBoardsDeletion"] = "false" - props["DataRetentionBoardsRetentionDays"] = "0" props["CustomUrlSchemes"] = strings.Join(c.DisplaySettings.CustomURLSchemes, ",") + props["MaxMarkdownNodes"] = strconv.FormatInt(int64(*c.DisplaySettings.MaxMarkdownNodes), 10) props["IsDefaultMarketplace"] = strconv.FormatBool(*c.PluginSettings.MarketplaceURL == model.PluginSettingsDefaultMarketplaceURL) props["ExperimentalSharedChannels"] = "false" props["CollapsedThreads"] = *c.ServiceSettings.CollapsedThreads @@ -200,8 +199,6 @@ func GenerateClientConfig(c *model.Config, telemetryID string, license *model.Li props["DataRetentionMessageRetentionDays"] = strconv.FormatInt(int64(*c.DataRetentionSettings.MessageRetentionDays), 10) props["DataRetentionEnableFileDeletion"] = strconv.FormatBool(*c.DataRetentionSettings.EnableFileDeletion) props["DataRetentionFileRetentionDays"] = strconv.FormatInt(int64(*c.DataRetentionSettings.FileRetentionDays), 10) - props["DataRetentionEnableBoardsDeletion"] = strconv.FormatBool(*c.DataRetentionSettings.EnableBoardsDeletion) - props["DataRetentionBoardsRetentionDays"] = strconv.FormatInt(int64(*c.DataRetentionSettings.BoardsRetentionDays), 10) } if license.HasSharedChannels() { @@ -244,6 +241,7 @@ func GenerateLimitedClientConfig(c *model.Config, telemetryID string, license *m props["WebsocketSecurePort"] = fmt.Sprintf("%v", *c.ServiceSettings.WebsocketSecurePort) props["EnableUserCreation"] = strconv.FormatBool(*c.TeamSettings.EnableUserCreation) props["EnableOpenServer"] = strconv.FormatBool(*c.TeamSettings.EnableOpenServer) + props["EnableJoinLeaveMessageByDefault"] = strconv.FormatBool(*c.TeamSettings.EnableJoinLeaveMessageByDefault) props["AndroidLatestVersion"] = c.ClientRequirements.AndroidLatestVersion props["AndroidMinVersion"] = c.ClientRequirements.AndroidMinVersion @@ -379,3 +377,17 @@ func GenerateLimitedClientConfig(c *model.Config, telemetryID string, license *m return props } + +func getGiphySdkKey(ss model.ServiceSettings) string { + if model.GetServiceEnvironment() == model.ServiceEnvironmentProduction { + if *ss.GiphySdkKey != "" { + return *ss.GiphySdkKey + } + + return model.MattermostGiphySdkKey + } else if model.GetServiceEnvironment() == model.ServiceEnvironmentDev || model.GetServiceEnvironment() == model.ServiceEnvironmentTest { + return model.ServiceSettingsDefaultGiphySdkKeyTest + } + + return "" +} diff --git a/server/config/client_test.go b/server/config/client_test.go index 7c1f7212bfd..0ccbae9413e 100644 --- a/server/config/client_test.go +++ b/server/config/client_test.go @@ -271,6 +271,41 @@ func TestGetClientConfig(t *testing.T) { "DisableAppBar": "true", }, }, + { + "default EnableJoinLeaveMessage", + &model.Config{}, + "tag1", + nil, + map[string]string{ + "EnableJoinLeaveMessageByDefault": "true", + }, + }, + { + "disable EnableJoinLeaveMessage", + &model.Config{ + TeamSettings: model.TeamSettings{ + EnableJoinLeaveMessageByDefault: model.NewBool(false), + }, + }, + "tag1", + nil, + map[string]string{ + "EnableJoinLeaveMessageByDefault": "false", + }, + }, + { + "test key for GiphySdkKey", + &model.Config{ + ServiceSettings: model.ServiceSettings{ + GiphySdkKey: model.NewString(""), + }, + }, + "", + nil, + map[string]string{ + "GiphySdkKey": model.ServiceSettingsDefaultGiphySdkKeyTest, + }, + }, } for _, testCase := range testCases { diff --git a/server/config/diff.go b/server/config/diff.go index ab744844c09..08535eac535 100644 --- a/server/config/diff.go +++ b/server/config/diff.go @@ -53,7 +53,6 @@ var configSensitivePaths = map[string]bool{ "MessageExportSettings.GlobalRelaySettings.SMTPUsername": true, "MessageExportSettings.GlobalRelaySettings.SMTPPassword": true, "MessageExportSettings.GlobalRelaySettings.EmailAddress": true, - "ServiceSettings.GfycatAPISecret": true, "ServiceSettings.SplitKey": true, "PluginSettings.Plugins": true, } diff --git a/server/config/diff_test.go b/server/config/diff_test.go index 3dd0bdc6d8a..43b6a07c1e3 100644 --- a/server/config/diff_test.go +++ b/server/config/diff_test.go @@ -464,27 +464,6 @@ func TestDiffSanitized(t *testing.T) { }, "", }, - { - "sensitive ServiceSettings.GfycatAPISecret", - func() *model.Config { - cfg := defaultConfigGen() - cfg.ServiceSettings.GfycatAPISecret = model.NewString("base") - return cfg - }(), - func() *model.Config { - cfg := defaultConfigGen() - cfg.ServiceSettings.GfycatAPISecret = model.NewString("actual") - return cfg - }(), - ConfigDiffs{ - { - Path: "ServiceSettings.GfycatAPISecret", - BaseVal: model.FakeSetting, - ActualVal: model.FakeSetting, - }, - }, - "", - }, { "sensitive ServiceSettings.SplitKey", func() *model.Config { diff --git a/server/config/utils.go b/server/config/utils.go index ba8de8de956..7e4e61e88fd 100644 --- a/server/config/utils.go +++ b/server/config/utils.go @@ -85,10 +85,6 @@ func desanitize(actual, target *model.Config) { *target.MessageExportSettings.GlobalRelaySettings.SMTPPassword = *actual.MessageExportSettings.GlobalRelaySettings.SMTPPassword } - if target.ServiceSettings.GfycatAPISecret != nil && *target.ServiceSettings.GfycatAPISecret == model.FakeSetting { - *target.ServiceSettings.GfycatAPISecret = *actual.ServiceSettings.GfycatAPISecret - } - if *target.ServiceSettings.SplitKey == model.FakeSetting { *target.ServiceSettings.SplitKey = *actual.ServiceSettings.SplitKey } @@ -120,14 +116,14 @@ func FixInvalidLocales(cfg *model.Config) bool { locales := i18n.GetSupportedLocales() if _, ok := locales[*cfg.LocalizationSettings.DefaultServerLocale]; !ok { + mlog.Warn("DefaultServerLocale must be one of the supported locales. Setting DefaultServerLocale to en as default value.", mlog.String("locale", *cfg.LocalizationSettings.DefaultServerLocale)) *cfg.LocalizationSettings.DefaultServerLocale = model.DefaultLocale - mlog.Warn("DefaultServerLocale must be one of the supported locales. Setting DefaultServerLocale to en as default value.") changed = true } if _, ok := locales[*cfg.LocalizationSettings.DefaultClientLocale]; !ok { + mlog.Warn("DefaultClientLocale must be one of the supported locales. Setting DefaultClientLocale to en as default value.", mlog.String("locale", *cfg.LocalizationSettings.DefaultClientLocale)) *cfg.LocalizationSettings.DefaultClientLocale = model.DefaultLocale - mlog.Warn("DefaultClientLocale must be one of the supported locales. Setting DefaultClientLocale to en as default value.") changed = true } @@ -164,13 +160,7 @@ func FixInvalidLocales(cfg *model.Config) bool { // Merge merges two configs together. The receiver's values are overwritten with the patch's // values except when the patch's values are nil. func Merge(cfg *model.Config, patch *model.Config, mergeConfig *utils.MergeConfig) (*model.Config, error) { - ret, err := utils.Merge(cfg, patch, mergeConfig) - if err != nil { - return nil, err - } - - retCfg := ret.(model.Config) - return &retCfg, nil + return utils.Merge(cfg, patch, mergeConfig) } func IsDatabaseDSN(dsn string) bool { diff --git a/server/einterfaces/account_migration.go b/server/einterfaces/account_migration.go index ccbf52df221..e142660b643 100644 --- a/server/einterfaces/account_migration.go +++ b/server/einterfaces/account_migration.go @@ -5,9 +5,10 @@ package einterfaces import ( "github.com/mattermost/mattermost/server/public/model" + "github.com/mattermost/mattermost/server/public/shared/request" ) type AccountMigrationInterface interface { - MigrateToLdap(fromAuthService string, foreignUserFieldNameToMatch string, force bool, dryRun bool) *model.AppError + MigrateToLdap(c *request.Context, fromAuthService string, foreignUserFieldNameToMatch string, force bool, dryRun bool) *model.AppError MigrateToSaml(fromAuthService string, usersMap map[string]string, auto bool, dryRun bool) *model.AppError } diff --git a/server/einterfaces/jobs/cloud_interface.go b/server/einterfaces/jobs/cloud_interface.go index 15214355cc5..96e0d022dc9 100644 --- a/server/einterfaces/jobs/cloud_interface.go +++ b/server/einterfaces/jobs/cloud_interface.go @@ -9,5 +9,5 @@ import ( type CloudJobInterface interface { MakeWorker() model.Worker - MakeScheduler() model.Scheduler + MakeScheduler() Scheduler } diff --git a/server/einterfaces/jobs/data_retention.go b/server/einterfaces/jobs/data_retention.go index 2a1e4b6a2a4..266dd2df343 100644 --- a/server/einterfaces/jobs/data_retention.go +++ b/server/einterfaces/jobs/data_retention.go @@ -9,5 +9,5 @@ import ( type DataRetentionJobInterface interface { MakeWorker() model.Worker - MakeScheduler() model.Scheduler + MakeScheduler() Scheduler } diff --git a/server/einterfaces/jobs/elasticsearch.go b/server/einterfaces/jobs/elasticsearch.go index 892880579e4..930452a2891 100644 --- a/server/einterfaces/jobs/elasticsearch.go +++ b/server/einterfaces/jobs/elasticsearch.go @@ -13,7 +13,7 @@ type ElasticsearchIndexerInterface interface { type ElasticsearchAggregatorInterface interface { MakeWorker() model.Worker - MakeScheduler() model.Scheduler + MakeScheduler() Scheduler } type ElasticsearchFixChannelIndexInterface interface { diff --git a/server/einterfaces/jobs/ldap_sync.go b/server/einterfaces/jobs/ldap_sync.go index bc1b20c7fb8..82b2986db15 100644 --- a/server/einterfaces/jobs/ldap_sync.go +++ b/server/einterfaces/jobs/ldap_sync.go @@ -9,5 +9,5 @@ import ( type LdapSyncInterface interface { MakeWorker() model.Worker - MakeScheduler() model.Scheduler + MakeScheduler() Scheduler } diff --git a/server/einterfaces/jobs/message_export.go b/server/einterfaces/jobs/message_export.go index b33d07585dd..00c3d25f739 100644 --- a/server/einterfaces/jobs/message_export.go +++ b/server/einterfaces/jobs/message_export.go @@ -9,5 +9,5 @@ import ( type MessageExportJobInterface interface { MakeWorker() model.Worker - MakeScheduler() model.Scheduler + MakeScheduler() Scheduler } diff --git a/server/einterfaces/jobs/scheduler.go b/server/einterfaces/jobs/scheduler.go new file mode 100644 index 00000000000..feb7fbc612e --- /dev/null +++ b/server/einterfaces/jobs/scheduler.go @@ -0,0 +1,17 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package jobs + +import ( + "time" + + "github.com/mattermost/mattermost/server/public/model" + "github.com/mattermost/mattermost/server/public/shared/request" +) + +type Scheduler interface { + Enabled(cfg *model.Config) bool + NextScheduleTime(cfg *model.Config, now time.Time, pendingJobs bool, lastSuccessfulJob *model.Job) *time.Time + ScheduleJob(c *request.Context, cfg *model.Config, pendingJobs bool, lastSuccessfulJob *model.Job) (*model.Job, *model.AppError) +} diff --git a/server/einterfaces/ldap.go b/server/einterfaces/ldap.go index 2e2bbdfed2a..48f5e6bd59e 100644 --- a/server/einterfaces/ldap.go +++ b/server/einterfaces/ldap.go @@ -5,7 +5,7 @@ package einterfaces import ( "github.com/mattermost/mattermost/server/public/model" - "github.com/mattermost/mattermost/server/v8/channels/app/request" + "github.com/mattermost/mattermost/server/public/shared/request" ) type LdapInterface interface { @@ -16,7 +16,7 @@ type LdapInterface interface { CheckPasswordAuthData(authData string, password string) *model.AppError CheckProviderAttributes(LS *model.LdapSettings, ouser *model.User, patch *model.UserPatch) string SwitchToLdap(userID, ldapID, ldapPassword string) *model.AppError - StartSynchronizeJob(waitForJobToFinish bool, includeRemovedMembers bool) (*model.Job, *model.AppError) + StartSynchronizeJob(c *request.Context, waitForJobToFinish bool, includeRemovedMembers bool) (*model.Job, *model.AppError) RunTest() *model.AppError GetAllLdapUsers() ([]*model.User, *model.AppError) MigrateIDAttribute(toAttribute string) error diff --git a/server/einterfaces/message_export.go b/server/einterfaces/message_export.go index 9ee0f150f10..c341f50d274 100644 --- a/server/einterfaces/message_export.go +++ b/server/einterfaces/message_export.go @@ -4,12 +4,11 @@ package einterfaces import ( - "context" - "github.com/mattermost/mattermost/server/public/model" + "github.com/mattermost/mattermost/server/public/shared/request" ) type MessageExportInterface interface { - StartSynchronizeJob(ctx context.Context, exportFromTimestamp int64) (*model.Job, *model.AppError) + StartSynchronizeJob(c *request.Context, exportFromTimestamp int64) (*model.Job, *model.AppError) RunExport(format string, since int64, limit int) (int64, *model.AppError) } diff --git a/server/einterfaces/mocks/AccountMigrationInterface.go b/server/einterfaces/mocks/AccountMigrationInterface.go index 508aa1982b9..c5f50a64fa2 100644 --- a/server/einterfaces/mocks/AccountMigrationInterface.go +++ b/server/einterfaces/mocks/AccountMigrationInterface.go @@ -6,6 +6,7 @@ package mocks import ( model "github.com/mattermost/mattermost/server/public/model" + request "github.com/mattermost/mattermost/server/public/shared/request" mock "github.com/stretchr/testify/mock" ) @@ -14,13 +15,13 @@ type AccountMigrationInterface struct { mock.Mock } -// MigrateToLdap provides a mock function with given fields: fromAuthService, foreignUserFieldNameToMatch, force, dryRun -func (_m *AccountMigrationInterface) MigrateToLdap(fromAuthService string, foreignUserFieldNameToMatch string, force bool, dryRun bool) *model.AppError { - ret := _m.Called(fromAuthService, foreignUserFieldNameToMatch, force, dryRun) +// MigrateToLdap provides a mock function with given fields: c, fromAuthService, foreignUserFieldNameToMatch, force, dryRun +func (_m *AccountMigrationInterface) MigrateToLdap(c *request.Context, fromAuthService string, foreignUserFieldNameToMatch string, force bool, dryRun bool) *model.AppError { + ret := _m.Called(c, fromAuthService, foreignUserFieldNameToMatch, force, dryRun) var r0 *model.AppError - if rf, ok := ret.Get(0).(func(string, string, bool, bool) *model.AppError); ok { - r0 = rf(fromAuthService, foreignUserFieldNameToMatch, force, dryRun) + if rf, ok := ret.Get(0).(func(*request.Context, string, string, bool, bool) *model.AppError); ok { + r0 = rf(c, fromAuthService, foreignUserFieldNameToMatch, force, dryRun) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(*model.AppError) diff --git a/server/einterfaces/mocks/AppContextInterface.go b/server/einterfaces/mocks/AppContextInterface.go deleted file mode 100644 index d97ff048915..00000000000 --- a/server/einterfaces/mocks/AppContextInterface.go +++ /dev/null @@ -1,118 +0,0 @@ -// Code generated by mockery v1.0.0. DO NOT EDIT. - -// Regenerate this file using `make einterfaces-mocks`. - -package mocks - -import ( - model "github.com/mattermost/mattermost/server/public/model" - mock "github.com/stretchr/testify/mock" -) - -// AppContextInterface is an autogenerated mock type for the AppContextInterface type -type AppContextInterface struct { - mock.Mock -} - -// AcceptLanguage provides a mock function with given fields: -func (_m *AppContextInterface) AcceptLanguage() string { - ret := _m.Called() - - var r0 string - if rf, ok := ret.Get(0).(func() string); ok { - r0 = rf() - } else { - r0 = ret.Get(0).(string) - } - - return r0 -} - -// IPAddress provides a mock function with given fields: -func (_m *AppContextInterface) IPAddress() string { - ret := _m.Called() - - var r0 string - if rf, ok := ret.Get(0).(func() string); ok { - r0 = rf() - } else { - r0 = ret.Get(0).(string) - } - - return r0 -} - -// Path provides a mock function with given fields: -func (_m *AppContextInterface) Path() string { - ret := _m.Called() - - var r0 string - if rf, ok := ret.Get(0).(func() string); ok { - r0 = rf() - } else { - r0 = ret.Get(0).(string) - } - - return r0 -} - -// RequestId provides a mock function with given fields: -func (_m *AppContextInterface) RequestId() string { - ret := _m.Called() - - var r0 string - if rf, ok := ret.Get(0).(func() string); ok { - r0 = rf() - } else { - r0 = ret.Get(0).(string) - } - - return r0 -} - -// Session provides a mock function with given fields: -func (_m *AppContextInterface) Session() *model.Session { - ret := _m.Called() - - var r0 *model.Session - if rf, ok := ret.Get(0).(func() *model.Session); ok { - r0 = rf() - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*model.Session) - } - } - - return r0 -} - -// T provides a mock function with given fields: translationID, args -func (_m *AppContextInterface) T(translationID string, args ...any) string { - var _ca []any - _ca = append(_ca, translationID) - _ca = append(_ca, args...) - ret := _m.Called(_ca...) - - var r0 string - if rf, ok := ret.Get(0).(func(string, ...any) string); ok { - r0 = rf(translationID, args...) - } else { - r0 = ret.Get(0).(string) - } - - return r0 -} - -// UserAgent provides a mock function with given fields: -func (_m *AppContextInterface) UserAgent() string { - ret := _m.Called() - - var r0 string - if rf, ok := ret.Get(0).(func() string); ok { - r0 = rf() - } else { - r0 = ret.Get(0).(string) - } - - return r0 -} diff --git a/server/einterfaces/mocks/CloudJobInterface.go b/server/einterfaces/mocks/CloudJobInterface.go index 9a609ff6374..9365c9095c2 100644 --- a/server/einterfaces/mocks/CloudJobInterface.go +++ b/server/einterfaces/mocks/CloudJobInterface.go @@ -5,8 +5,10 @@ package mocks import ( - model "github.com/mattermost/mattermost/server/public/model" + jobs "github.com/mattermost/mattermost/server/v8/einterfaces/jobs" mock "github.com/stretchr/testify/mock" + + model "github.com/mattermost/mattermost/server/public/model" ) // CloudJobInterface is an autogenerated mock type for the CloudJobInterface type @@ -15,15 +17,15 @@ type CloudJobInterface struct { } // MakeScheduler provides a mock function with given fields: -func (_m *CloudJobInterface) MakeScheduler() model.Scheduler { +func (_m *CloudJobInterface) MakeScheduler() jobs.Scheduler { ret := _m.Called() - var r0 model.Scheduler - if rf, ok := ret.Get(0).(func() model.Scheduler); ok { + var r0 jobs.Scheduler + if rf, ok := ret.Get(0).(func() jobs.Scheduler); ok { r0 = rf() } else { if ret.Get(0) != nil { - r0 = ret.Get(0).(model.Scheduler) + r0 = ret.Get(0).(jobs.Scheduler) } } diff --git a/server/einterfaces/mocks/DataRetentionJobInterface.go b/server/einterfaces/mocks/DataRetentionJobInterface.go index ea4bd29a692..8b6d90107fa 100644 --- a/server/einterfaces/mocks/DataRetentionJobInterface.go +++ b/server/einterfaces/mocks/DataRetentionJobInterface.go @@ -5,8 +5,10 @@ package mocks import ( - model "github.com/mattermost/mattermost/server/public/model" + jobs "github.com/mattermost/mattermost/server/v8/einterfaces/jobs" mock "github.com/stretchr/testify/mock" + + model "github.com/mattermost/mattermost/server/public/model" ) // DataRetentionJobInterface is an autogenerated mock type for the DataRetentionJobInterface type @@ -15,15 +17,15 @@ type DataRetentionJobInterface struct { } // MakeScheduler provides a mock function with given fields: -func (_m *DataRetentionJobInterface) MakeScheduler() model.Scheduler { +func (_m *DataRetentionJobInterface) MakeScheduler() jobs.Scheduler { ret := _m.Called() - var r0 model.Scheduler - if rf, ok := ret.Get(0).(func() model.Scheduler); ok { + var r0 jobs.Scheduler + if rf, ok := ret.Get(0).(func() jobs.Scheduler); ok { r0 = rf() } else { if ret.Get(0) != nil { - r0 = ret.Get(0).(model.Scheduler) + r0 = ret.Get(0).(jobs.Scheduler) } } diff --git a/server/einterfaces/mocks/ElasticsearchAggregatorInterface.go b/server/einterfaces/mocks/ElasticsearchAggregatorInterface.go index 09a59bdaa35..0f155f391b7 100644 --- a/server/einterfaces/mocks/ElasticsearchAggregatorInterface.go +++ b/server/einterfaces/mocks/ElasticsearchAggregatorInterface.go @@ -5,8 +5,10 @@ package mocks import ( - model "github.com/mattermost/mattermost/server/public/model" + jobs "github.com/mattermost/mattermost/server/v8/einterfaces/jobs" mock "github.com/stretchr/testify/mock" + + model "github.com/mattermost/mattermost/server/public/model" ) // ElasticsearchAggregatorInterface is an autogenerated mock type for the ElasticsearchAggregatorInterface type @@ -15,15 +17,15 @@ type ElasticsearchAggregatorInterface struct { } // MakeScheduler provides a mock function with given fields: -func (_m *ElasticsearchAggregatorInterface) MakeScheduler() model.Scheduler { +func (_m *ElasticsearchAggregatorInterface) MakeScheduler() jobs.Scheduler { ret := _m.Called() - var r0 model.Scheduler - if rf, ok := ret.Get(0).(func() model.Scheduler); ok { + var r0 jobs.Scheduler + if rf, ok := ret.Get(0).(func() jobs.Scheduler); ok { r0 = rf() } else { if ret.Get(0) != nil { - r0 = ret.Get(0).(model.Scheduler) + r0 = ret.Get(0).(jobs.Scheduler) } } diff --git a/server/einterfaces/mocks/LdapInterface.go b/server/einterfaces/mocks/LdapInterface.go index 32e9ab2bd92..c266abdc75b 100644 --- a/server/einterfaces/mocks/LdapInterface.go +++ b/server/einterfaces/mocks/LdapInterface.go @@ -6,7 +6,7 @@ package mocks import ( model "github.com/mattermost/mattermost/server/public/model" - request "github.com/mattermost/mattermost/server/v8/channels/app/request" + request "github.com/mattermost/mattermost/server/public/shared/request" mock "github.com/stretchr/testify/mock" ) @@ -334,25 +334,25 @@ func (_m *LdapInterface) RunTest() *model.AppError { return r0 } -// StartSynchronizeJob provides a mock function with given fields: waitForJobToFinish, includeRemovedMembers -func (_m *LdapInterface) StartSynchronizeJob(waitForJobToFinish bool, includeRemovedMembers bool) (*model.Job, *model.AppError) { - ret := _m.Called(waitForJobToFinish, includeRemovedMembers) +// StartSynchronizeJob provides a mock function with given fields: c, waitForJobToFinish, includeRemovedMembers +func (_m *LdapInterface) StartSynchronizeJob(c *request.Context, waitForJobToFinish bool, includeRemovedMembers bool) (*model.Job, *model.AppError) { + ret := _m.Called(c, waitForJobToFinish, includeRemovedMembers) var r0 *model.Job var r1 *model.AppError - if rf, ok := ret.Get(0).(func(bool, bool) (*model.Job, *model.AppError)); ok { - return rf(waitForJobToFinish, includeRemovedMembers) + if rf, ok := ret.Get(0).(func(*request.Context, bool, bool) (*model.Job, *model.AppError)); ok { + return rf(c, waitForJobToFinish, includeRemovedMembers) } - if rf, ok := ret.Get(0).(func(bool, bool) *model.Job); ok { - r0 = rf(waitForJobToFinish, includeRemovedMembers) + if rf, ok := ret.Get(0).(func(*request.Context, bool, bool) *model.Job); ok { + r0 = rf(c, waitForJobToFinish, includeRemovedMembers) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(*model.Job) } } - if rf, ok := ret.Get(1).(func(bool, bool) *model.AppError); ok { - r1 = rf(waitForJobToFinish, includeRemovedMembers) + if rf, ok := ret.Get(1).(func(*request.Context, bool, bool) *model.AppError); ok { + r1 = rf(c, waitForJobToFinish, includeRemovedMembers) } else { if ret.Get(1) != nil { r1 = ret.Get(1).(*model.AppError) diff --git a/server/einterfaces/mocks/LdapSyncInterface.go b/server/einterfaces/mocks/LdapSyncInterface.go index c5329297cba..e5f4ae9adae 100644 --- a/server/einterfaces/mocks/LdapSyncInterface.go +++ b/server/einterfaces/mocks/LdapSyncInterface.go @@ -5,8 +5,10 @@ package mocks import ( - model "github.com/mattermost/mattermost/server/public/model" + jobs "github.com/mattermost/mattermost/server/v8/einterfaces/jobs" mock "github.com/stretchr/testify/mock" + + model "github.com/mattermost/mattermost/server/public/model" ) // LdapSyncInterface is an autogenerated mock type for the LdapSyncInterface type @@ -15,15 +17,15 @@ type LdapSyncInterface struct { } // MakeScheduler provides a mock function with given fields: -func (_m *LdapSyncInterface) MakeScheduler() model.Scheduler { +func (_m *LdapSyncInterface) MakeScheduler() jobs.Scheduler { ret := _m.Called() - var r0 model.Scheduler - if rf, ok := ret.Get(0).(func() model.Scheduler); ok { + var r0 jobs.Scheduler + if rf, ok := ret.Get(0).(func() jobs.Scheduler); ok { r0 = rf() } else { if ret.Get(0) != nil { - r0 = ret.Get(0).(model.Scheduler) + r0 = ret.Get(0).(jobs.Scheduler) } } diff --git a/server/einterfaces/mocks/MessageExportInterface.go b/server/einterfaces/mocks/MessageExportInterface.go index e2066bcda23..06571fe1056 100644 --- a/server/einterfaces/mocks/MessageExportInterface.go +++ b/server/einterfaces/mocks/MessageExportInterface.go @@ -5,11 +5,9 @@ package mocks import ( - context "context" - - mock "github.com/stretchr/testify/mock" - model "github.com/mattermost/mattermost/server/public/model" + request "github.com/mattermost/mattermost/server/public/shared/request" + mock "github.com/stretchr/testify/mock" ) // MessageExportInterface is an autogenerated mock type for the MessageExportInterface type @@ -43,25 +41,25 @@ func (_m *MessageExportInterface) RunExport(format string, since int64, limit in return r0, r1 } -// StartSynchronizeJob provides a mock function with given fields: ctx, exportFromTimestamp -func (_m *MessageExportInterface) StartSynchronizeJob(ctx context.Context, exportFromTimestamp int64) (*model.Job, *model.AppError) { - ret := _m.Called(ctx, exportFromTimestamp) +// StartSynchronizeJob provides a mock function with given fields: c, exportFromTimestamp +func (_m *MessageExportInterface) StartSynchronizeJob(c *request.Context, exportFromTimestamp int64) (*model.Job, *model.AppError) { + ret := _m.Called(c, exportFromTimestamp) var r0 *model.Job var r1 *model.AppError - if rf, ok := ret.Get(0).(func(context.Context, int64) (*model.Job, *model.AppError)); ok { - return rf(ctx, exportFromTimestamp) + if rf, ok := ret.Get(0).(func(*request.Context, int64) (*model.Job, *model.AppError)); ok { + return rf(c, exportFromTimestamp) } - if rf, ok := ret.Get(0).(func(context.Context, int64) *model.Job); ok { - r0 = rf(ctx, exportFromTimestamp) + if rf, ok := ret.Get(0).(func(*request.Context, int64) *model.Job); ok { + r0 = rf(c, exportFromTimestamp) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(*model.Job) } } - if rf, ok := ret.Get(1).(func(context.Context, int64) *model.AppError); ok { - r1 = rf(ctx, exportFromTimestamp) + if rf, ok := ret.Get(1).(func(*request.Context, int64) *model.AppError); ok { + r1 = rf(c, exportFromTimestamp) } else { if ret.Get(1) != nil { r1 = ret.Get(1).(*model.AppError) diff --git a/server/einterfaces/mocks/MessageExportJobInterface.go b/server/einterfaces/mocks/MessageExportJobInterface.go index b39003ac3a3..e33dd12d48e 100644 --- a/server/einterfaces/mocks/MessageExportJobInterface.go +++ b/server/einterfaces/mocks/MessageExportJobInterface.go @@ -5,8 +5,10 @@ package mocks import ( - model "github.com/mattermost/mattermost/server/public/model" + jobs "github.com/mattermost/mattermost/server/v8/einterfaces/jobs" mock "github.com/stretchr/testify/mock" + + model "github.com/mattermost/mattermost/server/public/model" ) // MessageExportJobInterface is an autogenerated mock type for the MessageExportJobInterface type @@ -15,15 +17,15 @@ type MessageExportJobInterface struct { } // MakeScheduler provides a mock function with given fields: -func (_m *MessageExportJobInterface) MakeScheduler() model.Scheduler { +func (_m *MessageExportJobInterface) MakeScheduler() jobs.Scheduler { ret := _m.Called() - var r0 model.Scheduler - if rf, ok := ret.Get(0).(func() model.Scheduler); ok { + var r0 jobs.Scheduler + if rf, ok := ret.Get(0).(func() jobs.Scheduler); ok { r0 = rf() } else { if ret.Get(0) != nil { - r0 = ret.Get(0).(model.Scheduler) + r0 = ret.Get(0).(jobs.Scheduler) } } diff --git a/server/einterfaces/mocks/ResendInvitationEmailJobInterface.go b/server/einterfaces/mocks/ResendInvitationEmailJobInterface.go deleted file mode 100644 index 4d915ad284c..00000000000 --- a/server/einterfaces/mocks/ResendInvitationEmailJobInterface.go +++ /dev/null @@ -1,47 +0,0 @@ -// Code generated by mockery v1.0.0. DO NOT EDIT. - -// Regenerate this file using `make einterfaces-mocks`. - -package mocks - -import ( - model "github.com/mattermost/mattermost/server/public/model" - mock "github.com/stretchr/testify/mock" -) - -// ResendInvitationEmailJobInterface is an autogenerated mock type for the ResendInvitationEmailJobInterface type -type ResendInvitationEmailJobInterface struct { - mock.Mock -} - -// MakeScheduler provides a mock function with given fields: -func (_m *ResendInvitationEmailJobInterface) MakeScheduler() model.Scheduler { - ret := _m.Called() - - var r0 model.Scheduler - if rf, ok := ret.Get(0).(func() model.Scheduler); ok { - r0 = rf() - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(model.Scheduler) - } - } - - return r0 -} - -// MakeWorker provides a mock function with given fields: -func (_m *ResendInvitationEmailJobInterface) MakeWorker() model.Worker { - ret := _m.Called() - - var r0 model.Worker - if rf, ok := ret.Get(0).(func() model.Worker); ok { - r0 = rf() - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(model.Worker) - } - } - - return r0 -} diff --git a/server/einterfaces/mocks/SamlInterface.go b/server/einterfaces/mocks/SamlInterface.go index 3e9de0f0931..8ea64e85df9 100644 --- a/server/einterfaces/mocks/SamlInterface.go +++ b/server/einterfaces/mocks/SamlInterface.go @@ -6,7 +6,7 @@ package mocks import ( model "github.com/mattermost/mattermost/server/public/model" - request "github.com/mattermost/mattermost/server/v8/channels/app/request" + request "github.com/mattermost/mattermost/server/public/shared/request" mock "github.com/stretchr/testify/mock" ) diff --git a/server/einterfaces/mocks/Scheduler.go b/server/einterfaces/mocks/Scheduler.go new file mode 100644 index 00000000000..24011b8beac --- /dev/null +++ b/server/einterfaces/mocks/Scheduler.go @@ -0,0 +1,91 @@ +// Code generated by mockery v2.23.2. DO NOT EDIT. + +// Regenerate this file using `make einterfaces-mocks`. + +package mocks + +import ( + model "github.com/mattermost/mattermost/server/public/model" + request "github.com/mattermost/mattermost/server/public/shared/request" + mock "github.com/stretchr/testify/mock" + + time "time" +) + +// Scheduler is an autogenerated mock type for the Scheduler type +type Scheduler struct { + mock.Mock +} + +// Enabled provides a mock function with given fields: cfg +func (_m *Scheduler) Enabled(cfg *model.Config) bool { + ret := _m.Called(cfg) + + var r0 bool + if rf, ok := ret.Get(0).(func(*model.Config) bool); ok { + r0 = rf(cfg) + } else { + r0 = ret.Get(0).(bool) + } + + return r0 +} + +// NextScheduleTime provides a mock function with given fields: cfg, now, pendingJobs, lastSuccessfulJob +func (_m *Scheduler) NextScheduleTime(cfg *model.Config, now time.Time, pendingJobs bool, lastSuccessfulJob *model.Job) *time.Time { + ret := _m.Called(cfg, now, pendingJobs, lastSuccessfulJob) + + var r0 *time.Time + if rf, ok := ret.Get(0).(func(*model.Config, time.Time, bool, *model.Job) *time.Time); ok { + r0 = rf(cfg, now, pendingJobs, lastSuccessfulJob) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*time.Time) + } + } + + return r0 +} + +// ScheduleJob provides a mock function with given fields: c, cfg, pendingJobs, lastSuccessfulJob +func (_m *Scheduler) ScheduleJob(c *request.Context, cfg *model.Config, pendingJobs bool, lastSuccessfulJob *model.Job) (*model.Job, *model.AppError) { + ret := _m.Called(c, cfg, pendingJobs, lastSuccessfulJob) + + var r0 *model.Job + var r1 *model.AppError + if rf, ok := ret.Get(0).(func(*request.Context, *model.Config, bool, *model.Job) (*model.Job, *model.AppError)); ok { + return rf(c, cfg, pendingJobs, lastSuccessfulJob) + } + if rf, ok := ret.Get(0).(func(*request.Context, *model.Config, bool, *model.Job) *model.Job); ok { + r0 = rf(c, cfg, pendingJobs, lastSuccessfulJob) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*model.Job) + } + } + + if rf, ok := ret.Get(1).(func(*request.Context, *model.Config, bool, *model.Job) *model.AppError); ok { + r1 = rf(c, cfg, pendingJobs, lastSuccessfulJob) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).(*model.AppError) + } + } + + return r0, r1 +} + +type mockConstructorTestingTNewScheduler interface { + mock.TestingT + Cleanup(func()) +} + +// NewScheduler creates a new instance of Scheduler. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +func NewScheduler(t mockConstructorTestingTNewScheduler) *Scheduler { + mock := &Scheduler{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/server/einterfaces/saml.go b/server/einterfaces/saml.go index 35dfd3fedef..1417a07ad1c 100644 --- a/server/einterfaces/saml.go +++ b/server/einterfaces/saml.go @@ -5,7 +5,7 @@ package einterfaces import ( "github.com/mattermost/mattermost/server/public/model" - "github.com/mattermost/mattermost/server/v8/channels/app/request" + "github.com/mattermost/mattermost/server/public/shared/request" ) type SamlInterface interface { diff --git a/server/go.mod b/server/go.mod index 87c84e064dc..5b23edda01e 100644 --- a/server/go.mod +++ b/server/go.mod @@ -1,13 +1,13 @@ module github.com/mattermost/mattermost/server/v8 -go 1.19 +go 1.20 require ( code.sajari.com/docconv v1.3.5 github.com/Masterminds/semver/v3 v3.2.1 github.com/avct/uasurfer v0.0.0-20191028135549-26b5daa857f1 github.com/aws/aws-sdk-go v1.44.293 - github.com/blang/semver v3.5.1+incompatible + github.com/blang/semver/v4 v4.0.0 github.com/blevesearch/bleve/v2 v2.3.8 github.com/cespare/xxhash/v2 v2.2.0 github.com/dgryski/dgoogauth v0.0.0-20190221195224-5a805980a5f3 @@ -28,9 +28,7 @@ require ( github.com/graph-gophers/dataloader/v6 v6.0.0 github.com/graph-gophers/graphql-go v1.5.1-0.20230110080634-edea822f558a github.com/h2non/go-is-svg v0.0.0-20160927212452-35e8c4b0612c - github.com/hako/durafmt v0.0.0-20210608085754-5c1018a4e16b github.com/hashicorp/go-multierror v1.1.1 - github.com/hashicorp/memberlist v0.5.0 github.com/icrowley/fake v0.0.0-20221112152111-d7b7e2276db2 github.com/isacikgoz/prompt v0.1.0 github.com/jaytaylor/html2text v0.0.0-20230321000545-74c2419ad056 @@ -38,11 +36,9 @@ require ( github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80 github.com/lib/pq v1.10.9 github.com/mattermost/go-i18n v1.11.1-0.20211013152124-5c415071e404 - github.com/mattermost/gosaml2 v0.8.0 github.com/mattermost/gziphandler v0.0.1 - github.com/mattermost/ldap v0.0.0-20201202150706-ee0e6284187d - github.com/mattermost/logr/v2 v2.0.16 - github.com/mattermost/mattermost/server/public v0.0.6 + github.com/mattermost/logr/v2 v2.0.18 + github.com/mattermost/mattermost/server/public v0.0.9 github.com/mattermost/morph v1.0.5-0.20230511171014-e76e25978d56 github.com/mattermost/rsc v0.0.0-20160330161541-bbaefb05eaa0 github.com/mattermost/squirrel v0.2.0 @@ -52,7 +48,6 @@ require ( github.com/oov/psd v0.0.0-20220121172623-5db5eafcecbb github.com/opentracing/opentracing-go v1.2.0 github.com/pkg/errors v0.9.1 - github.com/prometheus/client_golang v1.16.0 github.com/reflog/dateconstraints v0.2.1 github.com/rs/cors v1.9.0 github.com/rudderlabs/analytics-go v3.3.3+incompatible @@ -64,21 +59,19 @@ require ( github.com/stretchr/testify v1.8.4 github.com/throttled/throttled v2.2.5+incompatible github.com/tinylib/msgp v1.1.8 - github.com/tylerb/graceful v1.2.15 github.com/uber/jaeger-client-go v2.30.0+incompatible github.com/uber/jaeger-lib v2.4.1+incompatible github.com/vmihailenco/msgpack/v5 v5.3.5 github.com/wiggin77/merror v1.0.5 github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c github.com/yuin/goldmark v1.5.4 - golang.org/x/crypto v0.10.0 + golang.org/x/crypto v0.12.0 golang.org/x/image v0.8.0 - golang.org/x/net v0.11.0 + golang.org/x/net v0.14.0 golang.org/x/sync v0.3.0 - golang.org/x/term v0.9.0 - golang.org/x/tools v0.10.0 + golang.org/x/term v0.11.0 + golang.org/x/tools v0.12.0 gopkg.in/mail.v2 v2.3.1 - gopkg.in/olivere/elastic.v6 v6.2.37 gopkg.in/yaml.v2 v2.4.0 ) @@ -91,10 +84,7 @@ require ( github.com/andybalholm/brotli v1.0.5 // indirect github.com/andybalholm/cascadia v1.3.2 // indirect github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de // indirect - github.com/armon/go-metrics v0.4.1 // indirect github.com/aymerick/douceur v0.2.0 // indirect - github.com/beevik/etree v1.2.0 // indirect - github.com/beorn7/perks v1.0.1 // indirect github.com/bits-and-blooms/bitset v1.8.0 // indirect github.com/bits-and-blooms/bloom/v3 v3.5.0 // indirect github.com/blevesearch/bleve_index_api v1.0.5 // indirect @@ -112,7 +102,6 @@ require ( github.com/blevesearch/zapx/v13 v13.3.8 // indirect github.com/blevesearch/zapx/v14 v14.3.8 // indirect github.com/blevesearch/zapx/v15 v15.3.11 // indirect - github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 // indirect github.com/corpix/uarand v0.2.0 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect github.com/davecgh/go-spew v1.1.1 // indirect @@ -121,7 +110,6 @@ require ( github.com/dustin/go-humanize v1.0.1 // indirect github.com/fatih/set v0.2.1 // indirect github.com/felixge/httpsnoop v1.0.3 // indirect - github.com/fortytw2/leaktest v1.3.0 // indirect github.com/francoispqt/gojay v1.2.13 // indirect github.com/gigawattio/window v0.0.0-20180317192513-0f5467e35573 // indirect github.com/go-asn1-ber/asn1-ber v1.5.4 // indirect @@ -130,24 +118,18 @@ require ( github.com/golang/protobuf v1.5.3 // indirect github.com/golang/snappy v0.0.4 // indirect github.com/gomodule/redigo v2.0.0+incompatible // indirect - github.com/google/btree v1.1.2 // indirect github.com/google/uuid v1.3.0 // indirect github.com/gopherjs/gopherjs v1.17.2 // indirect github.com/gorilla/css v1.0.0 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-hclog v1.5.0 // indirect - github.com/hashicorp/go-immutable-radix v1.3.1 // indirect - github.com/hashicorp/go-msgpack v1.1.5 // indirect github.com/hashicorp/go-plugin v1.4.10 // indirect - github.com/hashicorp/go-sockaddr v1.0.2 // indirect github.com/hashicorp/golang-lru v0.5.4 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/hashicorp/yamux v0.1.1 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/isacikgoz/fuzzy v0.2.0 // indirect github.com/jmespath/go-jmespath v0.4.0 // indirect - github.com/jonboulle/clockwork v0.4.0 // indirect - github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect github.com/klauspost/compress v1.16.6 // indirect @@ -157,14 +139,11 @@ require ( github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect github.com/levigross/exp-html v0.0.0-20120902181939-8df60c69a8f5 // indirect github.com/magiconair/properties v1.8.7 // indirect - github.com/mailru/easyjson v0.7.7 // indirect - github.com/mattermost/xml-roundtrip-validator v0.1.0 // indirect + github.com/mattermost/ldap v0.0.0-20201202150706-ee0e6284187d // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.19 // indirect github.com/mattn/go-runewidth v0.0.14 // indirect github.com/mattn/go-sqlite3 v2.0.3+incompatible // indirect - github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect - github.com/miekg/dns v1.1.55 // indirect github.com/minio/md5-simd v1.1.2 // indirect github.com/minio/sha256-simd v1.0.1 // indirect github.com/mitchellh/go-testing-interface v1.14.1 // indirect @@ -175,7 +154,6 @@ require ( github.com/nwaples/rardecode v1.1.3 // indirect github.com/oklog/run v1.1.0 // indirect github.com/olekukonko/tablewriter v0.0.5 // indirect - github.com/olivere/elastic v6.2.37+incompatible // indirect github.com/otiai10/gosseract/v2 v2.4.0 // indirect github.com/pborman/uuid v1.2.1 // indirect github.com/pelletier/go-toml v1.9.5 // indirect @@ -183,18 +161,14 @@ require ( github.com/philhofer/fwd v1.1.2 // indirect github.com/pierrec/lz4/v4 v4.1.18 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/prometheus/client_model v0.4.0 // indirect - github.com/prometheus/common v0.44.0 // indirect - github.com/prometheus/procfs v0.11.0 // indirect github.com/redis/go-redis/v9 v9.0.5 // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/richardlehane/mscfb v1.0.4 // indirect github.com/richardlehane/msoleps v1.0.3 // indirect github.com/rivo/uniseg v0.4.4 // indirect + github.com/rogpeppe/go-internal v1.10.0 // indirect github.com/rs/xid v1.5.0 // indirect - github.com/russellhaering/goxmldsig v1.4.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect - github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 // indirect github.com/segmentio/backo-go v1.0.1 // indirect github.com/spf13/afero v1.9.5 // indirect github.com/spf13/cast v1.5.1 // indirect @@ -214,9 +188,9 @@ require ( github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect go.etcd.io/bbolt v1.3.7 // indirect go.uber.org/atomic v1.11.0 // indirect - golang.org/x/mod v0.11.0 // indirect - golang.org/x/sys v0.9.0 // indirect - golang.org/x/text v0.10.0 // indirect + golang.org/x/mod v0.12.0 // indirect + golang.org/x/sys v0.11.0 // indirect + golang.org/x/text v0.12.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20230629202037-9506855d4529 // indirect google.golang.org/grpc v1.56.1 // indirect google.golang.org/protobuf v1.31.0 // indirect diff --git a/server/go.sum b/server/go.sum index 330797fdba0..908ad8ac086 100644 --- a/server/go.sum +++ b/server/go.sum @@ -61,7 +61,6 @@ dmitri.shuralyov.com/state v0.0.0-20180228185332-28bcc343414c/go.mod h1:0PRwlb0D git.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= -github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= github.com/HdrHistogram/hdrhistogram-go v0.9.0 h1:dpujRju0R4M/QZzcnR1LH1qm+TVG3UzkWdp5tH1WMcg= github.com/HdrHistogram/hdrhistogram-go v0.9.0/go.mod h1:nxrse8/Tzg2tg3DZcZjm6qEclQKK70g0KxO61gFFZD4= github.com/JalfResi/justext v0.0.0-20170829062021-c0282dea7198/go.mod h1:0SURuH1rsE8aVWvutuMZghRNrNrYEUzibzJfhEYR8L0= @@ -79,10 +78,6 @@ github.com/RoaringBitmap/roaring v1.3.0/go.mod h1:plvDsJQpxOC5bw8LRteu/MLWHsHez/ github.com/advancedlogic/GoOse v0.0.0-20191112112754-e742535969c1/go.mod h1:f3HCSN1fBWjcpGtXyM119MJgeQl838v6so/PQOqvE1w= github.com/advancedlogic/GoOse v0.0.0-20210820140952-9d5822d4a625 h1:LZIP5Bj5poWWRZ8fcL4ZwCupb4FwcTFK2RCTxkGnCX8= github.com/advancedlogic/GoOse v0.0.0-20210820140952-9d5822d4a625/go.mod h1:f3HCSN1fBWjcpGtXyM119MJgeQl838v6so/PQOqvE1w= -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/andybalholm/brotli v1.0.1/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y= github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs= github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= @@ -98,31 +93,20 @@ github.com/araddon/dateparse v0.0.0-20180729174819-cfd92a431d0e/go.mod h1:SLqhdZ github.com/araddon/dateparse v0.0.0-20200409225146-d820a6159ab1/go.mod h1:SLqhdZcd+dF3TEVL2RMoob5bBP5R1P1qkox+HtCBgGI= github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de h1:FxWPpzIjnTlhPwqqXc4/vE0f7GvRjuAsbW+HOIe8KnA= github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de/go.mod h1:DCaWoUhZrYW9p1lxo/cm8EmUOOzAPSEZNGF2DK1dJgw= -github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= -github.com/armon/go-metrics v0.4.1 h1:hR91U9KYmb6bLBYLQjyM+3j+rcd/UhE+G78SFnF8gJA= -github.com/armon/go-metrics v0.4.1/go.mod h1:E6amYzXo6aW1tqzoZGT755KkbgrJsSdpwZ+3JqfkOG4= -github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/avct/uasurfer v0.0.0-20191028135549-26b5daa857f1 h1:9h8f71kuF1pqovnn9h7LTHLEjxzyQaj0j1rQq5nsMM4= github.com/avct/uasurfer v0.0.0-20191028135549-26b5daa857f1/go.mod h1:noBAuukeYOXa0aXGqxr24tADqkwDO2KRD15FsuaZ5a8= github.com/aws/aws-sdk-go v1.44.293 h1:oBPrQqsyMYe61Sl/xKVvQFflXjPwYH11aKi8QR3Nhts= github.com/aws/aws-sdk-go v1.44.293/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI= 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/beevik/etree v1.1.0/go.mod h1:r8Aw8JqVegEf0w2fDnATrX9VpkMcyFeM0FhwO62wh+A= -github.com/beevik/etree v1.2.0 h1:l7WETslUG/T+xOPs47dtd6jov2Ii/8/OjCldk5fYfQw= -github.com/beevik/etree v1.2.0/go.mod h1:aiPf89g/1k3AShMVAzriilpcE4R/Vuor90y83zVZWFc= 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 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= -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/bits-and-blooms/bitset v1.2.0/go.mod h1:gIdJ4wp64HaoK2YrL1Q5/N7Y16edYb8uY+O0FJTyyDA= github.com/bits-and-blooms/bitset v1.8.0 h1:FD+XqgOZDUxxZ8hzoBFuV9+cGWY9CslN6d5MS5JVb4c= github.com/bits-and-blooms/bitset v1.8.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= github.com/bits-and-blooms/bloom/v3 v3.5.0 h1:AKDvi1V3xJCmSR6QhcBfHbCN4Vf8FfxeWkMNQfmAGhY= github.com/bits-and-blooms/bloom/v3 v3.5.0/go.mod h1:Y8vrn7nk1tPIlmLtW2ZPV+W7StdVMor6bC1xgpjMZFs= -github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ= -github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= +github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= +github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= github.com/blevesearch/bleve/v2 v2.3.8 h1:IqFyMJ73n4gY8AmVqM8Sa6EtAZ5beE8yramVqCvs2kQ= github.com/blevesearch/bleve/v2 v2.3.8/go.mod h1:Lh9aZEHrLKxwPnW4z4lsBEGnflZQ1V/aWP/t+htsiDw= github.com/blevesearch/bleve_index_api v1.0.5 h1:Lc986kpC4Z0/n1g3gg8ul7H+lxgOQPcXb9SxvQGu+tw= @@ -156,7 +140,6 @@ github.com/blevesearch/zapx/v14 v14.3.8/go.mod h1:vS6exLagv0vXmgpUbNRZC6UuEV0xwT github.com/blevesearch/zapx/v15 v15.3.11 h1:dstyZki9s10FNLsW4LpEvPQ+fmM3nX15h4wKfcBwnEg= github.com/blevesearch/zapx/v15 v15.3.11/go.mod h1:hiYbBDf5/Ud/Eji0faUmMTOyeOjcl8q1vWGgRe7+bIQ= github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 h1:DDGfHa7BWjL4YnC6+E63dPcxHo2sUxDIu8g3QgEJdRY= -github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4= github.com/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625/go.mod h1:HYsPBTaaSFSlLx/70C2HPIMNZpVV8+vt/A+FMnYP11g= github.com/bsm/ginkgo/v2 v2.7.0 h1:ItPMPH90RbmZJt5GtkcNvIRuGEdwlBItdNVoyzaNQao= github.com/bsm/gomega v1.26.0 h1:LhQm+AFcgV2M0WyKroMASzAzCAJVpAxQXv4SaI9a69Y= @@ -169,8 +152,6 @@ github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XL 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/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag= -github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I= 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= @@ -185,7 +166,6 @@ github.com/corpix/uarand v0.2.0 h1:U98xXwud/AVuCpkpgfPF7J5TQgr7R5tqT8VZP5KWbzE= github.com/corpix/uarand v0.2.0/go.mod h1:/3Z1QIqWkDIhf6XWn/08/uMHoQ8JUoTIKc2iPchBOmM= github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= -github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -212,7 +192,6 @@ github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.m github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fatih/color v1.12.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM= github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs= @@ -223,8 +202,6 @@ github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSw github.com/felixge/httpsnoop v1.0.3 h1:s/nj+GCswXYzN5v2DpNMuMQYe+0DDwt5WVCU6CWBdXk= github.com/felixge/httpsnoop v1.0.3/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= -github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw= -github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g= github.com/francoispqt/gojay v1.2.13 h1:d2m3sFjloqoIUQU3TsHBgj6qg/BVGlTBeHDUmyJnXKk= github.com/francoispqt/gojay v1.2.13/go.mod h1:ehT5mTG4ua4581f1++1WLG0vPdaA9HaiDsoyrBGkyDY= github.com/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0XL9UY= @@ -245,10 +222,6 @@ github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxI 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/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-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-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= @@ -259,7 +232,6 @@ github.com/go-resty/resty/v2 v2.7.0/go.mod h1:9PWDzw47qPphMRFfhsyk0NnSgvluHcljSM github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI= github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= -github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/golang-jwt/jwt/v5 v5.0.0 h1:1n1XNM9hk7O9mnQoNBGolZvzebBQ7p93ULHRc28XJUE= github.com/golang-jwt/jwt/v5 v5.0.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= @@ -311,8 +283,6 @@ github.com/gomodule/redigo v2.0.0+incompatible h1:K/R+8tc58AaqLkqG2Ol3Qk+DR/TlNu github.com/gomodule/redigo v2.0.0+incompatible/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU= -github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= 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= @@ -384,39 +354,21 @@ github.com/grpc-ecosystem/grpc-gateway v1.5.0/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpg github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= github.com/h2non/go-is-svg v0.0.0-20160927212452-35e8c4b0612c h1:fEE5/5VNnYUoBOj2I9TP8Jc+a7lge3QWn9DKE7NCwfc= github.com/h2non/go-is-svg v0.0.0-20160927212452-35e8c4b0612c/go.mod h1:ObS/W+h8RYb1Y7fYivughjxojTmIu5iAIjSrSLCLeqE= -github.com/hako/durafmt v0.0.0-20210608085754-5c1018a4e16b h1:wDUNC2eKiL35DbLvsDhiblTUXHxcOPwQSCzi7xpQUN4= -github.com/hako/durafmt v0.0.0-20210608085754-5c1018a4e16b/go.mod h1:VzxiSdG6j1pi7rwGm/xYI5RbtpBgM8sARDXlvEvxlu0= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= -github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= github.com/hashicorp/go-hclog v1.5.0 h1:bI2ocEMgcVlz55Oj1xZNBsVi900c7II+fWDyV9o+13c= github.com/hashicorp/go-hclog v1.5.0/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= -github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= -github.com/hashicorp/go-immutable-radix v1.3.1 h1:DKHmCUm2hRBK510BaiZlwvpD40f8bJFeZnpfm2KLowc= -github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= -github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= -github.com/hashicorp/go-msgpack v1.1.5 h1:9byZdVjKTe5mce63pRVNP1L7UAmdHOTEMGehn6KvJWs= -github.com/hashicorp/go-msgpack v1.1.5/go.mod h1:gWVc3sv/wbDmR3rQsj1CAktEZzoz1YNK9NfGLXJ69/4= -github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= github.com/hashicorp/go-plugin v1.4.10 h1:xUbmA4jC6Dq163/fWcp8P3JuHilrHHMLNRxzGQJ9hNk= github.com/hashicorp/go-plugin v1.4.10/go.mod h1:6/1TEzT0eQznvI/gV2CM29DLSkAK/e58mUWKVsPaph0= -github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs= -github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= -github.com/hashicorp/go-sockaddr v1.0.2 h1:ztczhD1jLxIRjVejw8gFomI1BQZOe2WoVOu0SyteCQc= -github.com/hashicorp/go-sockaddr v1.0.2/go.mod h1:rB4wwRAUzs07qva3c5SdrY/NEtAUjGlgmH/UkBUC97A= -github.com/hashicorp/go-uuid v1.0.0 h1:RS8zrF7PhGwyNPOtxSClXXj9HA8feRnJzgnI1RJCSnM= -github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= 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 v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc= github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= 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/memberlist v0.5.0 h1:EtYPN8DpAURiapus508I4n9CzHs2W+8NZGbmmR/prTM= -github.com/hashicorp/memberlist v0.5.0/go.mod h1:yvyXLpo0QaGE59Y7hDTsTzDD25JYBZ4mHgHUZ8lrOI0= github.com/hashicorp/yamux v0.1.1 h1:yrQxtgseBDrq9Y652vSRDvsKCJKOUD+GzTS4Y0Y8pvE= github.com/hashicorp/yamux v0.1.1/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= @@ -441,18 +393,11 @@ github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGw github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g= github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ= -github.com/jonboulle/clockwork v0.2.2/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8= -github.com/jonboulle/clockwork v0.4.0 h1:p4Cf1aMWXnXAUh8lVfewRBx1zaTSYKrKMF2g3ST4RZ4= -github.com/jonboulle/clockwork v0.4.0/go.mod h1:xgRqUGwRcjKCO1vbZUEtSLrqKoPSsUpK7fnezOII0kc= -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= -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/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/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= @@ -468,18 +413,13 @@ github.com/klauspost/cpuid/v2 v2.2.5/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZY github.com/klauspost/pgzip v1.2.5/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs= github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU= github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs= -github.com/konsorten/go-windows-terminal-sequences v1.0.1/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/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.3/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= -github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 h1:SOEGU9fKiNWd/HOJuq6+3iTQz8KNCLtVX6idSoTLdUw= @@ -497,35 +437,27 @@ github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm 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.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= -github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= -github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mattermost/go-i18n v1.11.1-0.20211013152124-5c415071e404 h1:Khvh6waxG1cHc4Cz5ef9n3XVCxRWpAKUtqg9PJl5+y8= github.com/mattermost/go-i18n v1.11.1-0.20211013152124-5c415071e404/go.mod h1:RyS7FDNQlzF1PsjbJWHRI35exqaKGSO9qD4iv8QjE34= -github.com/mattermost/gosaml2 v0.8.0 h1:nkYiByawqwJ7KncK1LDWKwTx5aRarBTQsmH+XcCVsWQ= -github.com/mattermost/gosaml2 v0.8.0/go.mod h1:1nMAdE2Psxaz+pj79Oytayi+hC3aZUi3SmJQlIe+sLM= github.com/mattermost/gziphandler v0.0.1 h1:uXHcXF5agnQ6bXabvpiwwwZOlCYoa7mKHH0lxns/o8w= github.com/mattermost/gziphandler v0.0.1/go.mod h1:CvvZR7sXqhj81V2swXuQY7T04Ccc89u7W7pHNPKev8g= github.com/mattermost/ldap v0.0.0-20201202150706-ee0e6284187d h1:/RJ/UV7M5c7L2TQ0KNm4yZxxFvC1nvRz/gY/Daa35aI= github.com/mattermost/ldap v0.0.0-20201202150706-ee0e6284187d/go.mod h1:HLbgMEI5K131jpxGazJ97AxfPDt31osq36YS1oxFQPQ= -github.com/mattermost/logr/v2 v2.0.16 h1:jnePX4cPskC3WDFvUardh/xZfxNdsFXbEERJQ1kUEDE= -github.com/mattermost/logr/v2 v2.0.16/go.mod h1:1dm/YhTpozsqANXxo5Pi5zYLBsal2xY0pX+JZNbzYJY= -github.com/mattermost/mattermost/server/public v0.0.6 h1:FUaJ+P36E3Tt12Umdm8p1h7sZNUeObDk3p3aFTaBkCo= -github.com/mattermost/mattermost/server/public v0.0.6/go.mod h1:Y7Ht1haGGrsuYzX73HhpSe2VnbGLuZj2/tsQslHd2/M= +github.com/mattermost/logr/v2 v2.0.18 h1:qiznuwwKckZJoGtBYc4Y9FAY97/oQwV1Pq9oO5qP5nk= +github.com/mattermost/logr/v2 v2.0.18/go.mod h1:1dm/YhTpozsqANXxo5Pi5zYLBsal2xY0pX+JZNbzYJY= +github.com/mattermost/mattermost/server/public v0.0.9 h1:Qsktgxx5dc8xVAUHP5MbSLi6Cf82iB/83r6S9bluHto= +github.com/mattermost/mattermost/server/public v0.0.9/go.mod h1:sgXQrYzs+IJy51mB8E8OBljagk2u3YwQRoYlBH5goiw= github.com/mattermost/morph v1.0.5-0.20230511171014-e76e25978d56 h1:SjFYbWvmuf73d/KaYlnHosPpB3/k38ebr1WWw9X5XKc= github.com/mattermost/morph v1.0.5-0.20230511171014-e76e25978d56/go.mod h1:gD+EaqX2UMyyuzmF4PFh4r33XneQ8Nzi+0E8nXjMa3A= github.com/mattermost/rsc v0.0.0-20160330161541-bbaefb05eaa0 h1:G9tL6JXRBMzjuD1kkBtcnd42kUiT6QDwxfFYu7adM6o= github.com/mattermost/rsc v0.0.0-20160330161541-bbaefb05eaa0/go.mod h1:nV5bfVpT//+B1RPD2JvRnxbkLmJEYXmRaaVl15fsXjs= github.com/mattermost/squirrel v0.2.0 h1:8ZWeyf+MWQ2cL7hu9REZgLtz2IJi51qqZEovI3T3TT8= github.com/mattermost/squirrel v0.2.0/go.mod h1:NPPtk+CdpWre4GxMGoOpzEVFVc0ZoEFyJBZGCtn9nSU= -github.com/mattermost/xml-roundtrip-validator v0.1.0 h1:RXbVD2UAl7A7nOTR4u7E3ILa4IbtvKBHw64LDsmu9hU= -github.com/mattermost/xml-roundtrip-validator v0.1.0/go.mod h1:qccnGMcpgwcNaBnxqpJpWWUiPNr5H3O8eDgGV9gT5To= -github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= -github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.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/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= @@ -541,38 +473,29 @@ github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A 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/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= -github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= github.com/mholt/archiver/v3 v3.5.1 h1:rDjOBX9JSF5BvoJGvjqK479aL70qh9DIpZCl+k7Clwo= github.com/mholt/archiver/v3 v3.5.1/go.mod h1:e3dqJ7H78uzsRSEACH1joayhuSyhnonssnDhppzS1L4= github.com/microcosm-cc/bluemonday v1.0.1/go.mod h1:hsXNsILzKxV+sX77C5b8FSuKF00vh2OMYv+xgHpAMF4= github.com/microcosm-cc/bluemonday v1.0.24 h1:NGQoPtwGVcbGkKfvyYk1yRqknzBuoMiUrO6R7uFTPlw= github.com/microcosm-cc/bluemonday v1.0.24/go.mod h1:ArQySAMps0790cHSkdPEJ7bGkF2VePWH773hsJNSHf8= -github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso= -github.com/miekg/dns v1.1.55 h1:GoQ4hpsj0nFLYe+bWiCToyrBEJXkQfOOIvFGFy0lEgo= -github.com/miekg/dns v1.1.55/go.mod h1:uInx36IzPl7FYnDcMeVWxj9byh7DutNykX4G9Sj60FY= github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34= github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM= github.com/minio/minio-go/v7 v7.0.59 h1:lxIXwsTIcQkYoEG25rUJbzpmSB/oWeVDmxFo/uWUUsw= github.com/minio/minio-go/v7 v7.0.59/go.mod h1:NUDy4A4oXPq1l2yK6LTSvCEzAMeIcoz9lcj5dbzSrRE= github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM= github.com/minio/sha256-simd v1.0.1/go.mod h1:Pz6AKMiUdngCLpeTL/RJY1M9rUuPMYujV5xJjtbRSN8= -github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= github.com/mitchellh/go-testing-interface v1.14.1 h1:jrgshOhYAUVNMAJiKbEu7EqAwgJJ2JqpQmpLJOu07cU= github.com/mitchellh/go-testing-interface v1.14.1/go.mod h1:gfgS7OtZj6MA4U1UrDRp04twqAjfvlZyCfX3sDjEym8= -github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= 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/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= -github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/mschoch/smat v0.2.0 h1:8imxQsjDm8yFEAVBe7azKmKSgzSkZXDuKkSq9374khM= github.com/mschoch/smat v0.2.0/go.mod h1:kc9mz7DoBKqDyiRL7VZN8KvXQMWeTaVnttLRXOlotKw= -github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86/go.mod h1:kHJEU3ofeGjhHklVoIGuVj85JJwZ6kWPaJwCIxgnFmo= github.com/neelance/sourcemap v0.0.0-20151028013722-8c68805598ab/go.mod h1:Qr6/a/Q4r9LP1IltGz7tA7iOK1WonHEYhu1HRBA7ZiM= github.com/nwaples/rardecode v1.1.0/go.mod h1:5DzqNKiOdpKKBH87u8VlvAnPZMXcGRhxWkRpHbbfGS0= @@ -584,8 +507,6 @@ github.com/olekukonko/tablewriter v0.0.0-20180506121414-d4647c9c7a84/go.mod h1:v github.com/olekukonko/tablewriter v0.0.4/go.mod h1:zq6QwlOf5SlnkVbMSr5EoBv3636FWnp+qbPhuoO21uA= github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= -github.com/olivere/elastic v6.2.37+incompatible h1:UfSGJem5czY+x/LqxgeCBgjDn6St+z8OnsCuxwD3L0U= -github.com/olivere/elastic v6.2.37+incompatible/go.mod h1:J+q1zQJTgAz9woqsbVRqGeB5G1iqDKVBWLNSYW8yfJ8= github.com/oov/psd v0.0.0-20220121172623-5db5eafcecbb h1:JF9kOhBBk4WPF7luXFu5yR+WgaFm9L/KiHJHhU9vDwA= github.com/oov/psd v0.0.0-20220121172623-5db5eafcecbb/go.mod h1:GHI1bnmAcbp96z6LNfBJvtrjxhaXGkbsk967utPlvL8= github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs= @@ -599,9 +520,6 @@ github.com/otiai10/gosseract/v2 v2.4.0/go.mod h1:fhbIDRh29bj13vni6RT3gtWKjKCAeqD github.com/otiai10/mint v1.3.0/go.mod h1:F5AjcsTsWUqX+Na9fpHb52P8pcRX2CI6A3ctIT91xUo= github.com/otiai10/mint v1.3.3 h1:7JgpsBaN0uMkyju4tbYHu0mnM55hNKVYLsXmwr15NQI= github.com/otiai10/mint v1.3.3/go.mod h1:/yxELlJQ0ufhjUwhshSj+wFjZ78CnZ48/1wtmBH1OTc= -github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= -github.com/pascaldekloe/goe v0.1.0 h1:cBOtyMzM9HTpWjXfbbunk26uA6nG3a8n06Wieeh0MwY= -github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= github.com/pborman/uuid v1.2.1 h1:+ZZIw58t/ozdjRaXh/3awHfmWRbzYxJoAdNJxe/3pvw= github.com/pborman/uuid v1.2.1/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= @@ -616,38 +534,17 @@ github.com/pierrec/lz4/v4 v4.1.2/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuR github.com/pierrec/lz4/v4 v4.1.18 h1:xaKrnTkyoqfh1YItXl56+6KJNVYWlEEPuAQW9xsplYQ= github.com/pierrec/lz4/v4 v4.1.18/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4= -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/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/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= github.com/prometheus/client_golang v0.8.0/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= -github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= -github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= -github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU= -github.com/prometheus/client_golang v1.16.0 h1:yk/hx9hDbrGHovbci4BY+pRMfSuuat626eFsHb7tmT8= -github.com/prometheus/client_golang v1.16.0/go.mod h1:Zsulrv/L9oM40tJ7T815tM89lFEugiJ9HzIqaAx4LKc= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= -github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/client_model v0.4.0 h1:5lQXD3cAg1OXBf4Wq03gTrXHeaV0TQvGfUooCfx1yqY= -github.com/prometheus/client_model v0.4.0/go.mod h1:oMQmHW1/JoDwqLtg57MGgP/Fb1CJEYF2imWWhWtMkYU= github.com/prometheus/common v0.0.0-20180801064454-c7de2306084e/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= -github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= -github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4= -github.com/prometheus/common v0.44.0 h1:+5BrQJwiBB9xsMygAB3TNvpQKOwlkc25LbISbrdOOfY= -github.com/prometheus/common v0.44.0/go.mod h1:ofAIvZbQ1e/nugmZGz4/qCb9Ap1VoSTIO7x0VV9VvuY= github.com/prometheus/procfs v0.0.0-20180725123919-05ee40e3a273/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= -github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= -github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= -github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= -github.com/prometheus/procfs v0.11.0 h1:5EAgkfkMl659uZPbe9AS2N68a7Cc1TJbPEuGzFuRbyk= -github.com/prometheus/procfs v0.11.0/go.mod h1:nwNm2aOCAYw8uTR/9bWRREkZFxAUcWzPHWJq+XBB/FM= github.com/redis/go-redis/v9 v9.0.5 h1:CuQcn5HIEeK7BgElubPP8CGtE0KakrnbBSTLjathl5o= github.com/redis/go-redis/v9 v9.0.5/go.mod h1:WqMKv5vnQbRuZstUwxQI195wHy+t4PuXDOjzMvcuQHk= github.com/reflog/dateconstraints v0.2.1 h1:Hz1n2Q1vEm0Rj5gciDQcCN1iPBwfFjxUJy32NknGP/s= @@ -666,27 +563,20 @@ github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis= github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= -github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= -github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= +github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/rs/cors v1.9.0 h1:l9HGsTsHJcvW14Nk7J9KFz8bzeAWXn3CG6bgt7LsrAE= github.com/rs/cors v1.9.0/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= github.com/rs/xid v1.5.0 h1:mKX4bl4iPYJtEIxp6CYiUuLQ/8DYMoz0PUdtGgMFRVc= github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= github.com/rudderlabs/analytics-go v3.3.3+incompatible h1:OG0XlKoXfr539e2t1dXtTB+Gr89uFW+OUNQBVhHIIBY= github.com/rudderlabs/analytics-go v3.3.3+incompatible/go.mod h1:LF8/ty9kUX4PTY3l5c97K3nZZaX5Hwsvt+NBaRL/f30= -github.com/russellhaering/goxmldsig v1.2.0/go.mod h1:gM4MDENBQf7M+V824SGfyIUVFWydB7n0KkEubVJl+Tw= -github.com/russellhaering/goxmldsig v1.4.0 h1:8UcDh/xGyQiyrW+Fq5t8f+l2DLB1+zlhYzkPUJ7Qhys= -github.com/russellhaering/goxmldsig v1.4.0/go.mod h1:gM4MDENBQf7M+V824SGfyIUVFWydB7n0KkEubVJl+Tw= github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd h1:CmH9+J6ZSsIjUK3dcGsnCnO41eRBOnY12zwkn5qVwgc= github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd/go.mod h1:hPqNNc0+uJM6H+SuU8sEs5K5IQeKccPqeSjfgcKGgPk= -github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/scylladb/termtables v0.0.0-20191203121021-c4c0b6d42ff4/go.mod h1:C1a7PQSMz9NShzorzCiG2fk9+xuCgLkPeCvMHYR2OWg= -github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 h1:nn5Wsu0esKSJiIVhscUtVbo7ada43DJhG55ua/hjS5I= -github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= github.com/segmentio/backo-go v1.0.1 h1:68RQccglxZeyURy93ASB/2kc9QudzgIDexJ927N++y4= github.com/segmentio/backo-go v1.0.1/go.mod h1:9/Rh6yILuLysoQnZ2oNooD2g7aBnvM7r/fNVxRNWfBc= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= @@ -713,8 +603,6 @@ github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95/go. github.com/shurcooL/users v0.0.0-20180125191416-49c67e49c537/go.mod h1:QJTqeLYEDaXHZDBsXlPCDqdhQuJkuw4NOtaxYe3xii4= github.com/shurcooL/webdavfs v0.0.0-20170829043945-18c3829fa133/go.mod h1:hKmq5kWdCj2z2KEozexVbfEZIWiTjhE0+UjmZgPqehw= github.com/simplereach/timeutils v1.2.0/go.mod h1:VVbQDfN/FHRZa1LSqcwo4kNZ62OOyqLLGQKYB3pB0Q8= -github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= -github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d/go.mod h1:UdhH50NIW0fCiwBSr0co2m7BnFLdv4fQTgdqdJTHFeE= @@ -741,7 +629,6 @@ github.com/splitio/go-toolkit/v5 v5.3.1/go.mod h1:xYhUvV1gga9/1029Wbp5pjnR6Cy8nv github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf h1:pvbZ0lM0XWPBqUKqFU8cmavspvIl9nulOYwdy6IFRRo= github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf/go.mod h1:RJID2RhlZKId02nZ62WenDCkgHFerpIOmW0iT7GKmXM= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= @@ -771,11 +658,8 @@ github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/tinylib/msgp v1.1.8 h1:FCXC1xanKO4I8plpHGH2P7koL/RzZs12l/+r7vakfm0= github.com/tinylib/msgp v1.1.8/go.mod h1:qkpG+2ldGg4xRFmx+jfTvZPxfGFhi64BcnL9vkCm/Tw= -github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= github.com/twmb/murmur3 v1.1.6 h1:mqrRot1BRxm+Yct+vavLMou2/iJt0tNVTTC0QoIjaZg= github.com/twmb/murmur3 v1.1.6/go.mod h1:Qq/R7NUyOfr65zD+6Q5IHKsJLwP7exErjN6lyyq3OSQ= -github.com/tylerb/graceful v1.2.15 h1:B0x01Y8fsJpogzZTkDg6BDi6eMf03s01lEKGdrv83oA= -github.com/tylerb/graceful v1.2.15/go.mod h1:LPYTbOYmUTdabwRt0TGhLllQ0MUNbs0Y5q1WXJOI9II= github.com/uber/jaeger-client-go v2.30.0+incompatible h1:D6wyKGCecFaSRUpo8lCVbaOOb6ThwMmTEbhRwtKR97o= github.com/uber/jaeger-client-go v2.30.0+incompatible/go.mod h1:WVhlPFC8FDjOFMMWRy2pZqQJSXxYSwNYOkTr/Z6d3Kk= github.com/uber/jaeger-lib v2.4.1+incompatible h1:td4jdvLcExb4cBISKIpHuGoVXh+dVKhn2Um6rjCsSsg= @@ -823,20 +707,18 @@ go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= go4.org v0.0.0-20180809161055-417644f6feb5/go.mod h1:MkTOUMDaeVYJUOUsaDXIhWPZYa1yOyC1qaOBpL57BhE= golang.org/x/build v0.0.0-20190111050920-041ab4dc3f9d/go.mod h1:OWs+y06UdEOHN4y+MfF/py+xQ/tYqIWW03b70/CG9Rw= -golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/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-20190313024323-a1f597ede03a/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 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-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.10.0 h1:LKqV2xt9+kDzSTfOhx4FrkEBcMrAgHSYgzywV9zcGmM= -golang.org/x/crypto v0.10.0/go.mod h1:o4eNf7Ede1fv+hwOwZsTHl9EsPFO6q6ZvYR8vYfY45I= +golang.org/x/crypto v0.12.0 h1:tFM/ta59kqch6LlvYnPa0yx5a83cL2nHflFhYKvv9Yk= +golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -879,15 +761,14 @@ golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.11.0 h1:bUO06HqtnRcc/7l71XBe4WcqTZ+3AH1J59zWDDwLKgU= -golang.org/x/mod v0.11.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc= +golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181029044818-c44066c5c816/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181106065722-10aee1819953/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 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= @@ -896,11 +777,9 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn 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-20190923162816-aa69164e4478/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= @@ -935,8 +814,8 @@ golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= -golang.org/x/net v0.11.0 h1:Gi2tvZIJyBtO9SDr1q9h5hEQCp/4L2RQ+ar0qjx2oNU= -golang.org/x/net v0.11.0/go.mod h1:2L/ixqYpgIVXmeoSA/4Lu7BzTG4KIyPIryS4IsOd1oQ= +golang.org/x/net v0.14.0 h1:BONx9s002vGdD9umnlX1Po8vOZmrgH34qlHcD1MfK14= +golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20181017192945-9dcd33a902f4/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= @@ -971,24 +850,18 @@ golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E= golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= -golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181029174526-d69651ed3497/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/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-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190316082340-a2f829d7f35f/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-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -1038,7 +911,6 @@ golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -1046,16 +918,16 @@ golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.9.0 h1:KS/R3tvhPqvJvwcKfnBHJwwthS11LRhmM5D59eEXa0s= -golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM= +golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY= -golang.org/x/term v0.9.0 h1:GRRCnKYhdQrD8kfRAdQ6Zcw1P0OcELxGLKJvtjVMZ28= -golang.org/x/term v0.9.0/go.mod h1:M6DEAAIenWoTxdKrOltXcmDY3rSplQUkrvaDU5FcQyo= +golang.org/x/term v0.11.0 h1:F9tnn/DA/Im8nCwm+fX+1/eBwi4qFjRT++MhtVC4ZX0= +golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 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= @@ -1069,8 +941,9 @@ golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.10.0 h1:UpjohKhiEgNc0CSauXmwYftY1+LlaC75SJwh0SgCX58= golang.org/x/text v0.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.12.0 h1:k+n5B8goJNdU7hSvEtMUz3d1Q6D/XW4COJSJR6fN0mc= +golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/time v0.0.0-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= @@ -1083,7 +956,6 @@ golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3 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-20190424220101-1e8e1cfdf96b/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190425150028-36563e24a262/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= @@ -1091,7 +963,6 @@ golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgw 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-20190907020128-2ca718005c18/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-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= @@ -1134,8 +1005,8 @@ golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.4.0/go.mod h1:UE5sM2OK9E/d67R0ANs2xJizIymRP5gJU295PvKXxjQ= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/tools v0.10.0 h1:tvDr/iQoUqNdohiYm0LmmKcBk+q86lb9EprIUFhHHGg= -golang.org/x/tools v0.10.0/go.mod h1:UJwyiVBsOA2uwvK/e5OY3GTpDUJriEd+/YlqAwLPmyM= +golang.org/x/tools v0.12.0 h1:YW6HUoUmYBpwSgyaGaZq1fHjrBjX1rlpZ54T6mu2kss= +golang.org/x/tools v0.12.0/go.mod h1:Sc0INKfu04TlqNoRA1hgpFZbhYXHPr4V5DzpSBTPqQM= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -1303,14 +1174,11 @@ google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQ google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= -gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk= gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk= 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-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/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= @@ -1320,18 +1188,13 @@ gopkg.in/mail.v2 v2.3.1/go.mod h1:htwXN1Qh09vZJ1NVKxQqHPBaCBbzKhp5GzuJEA4VJWw= gopkg.in/mgo.v2 v2.0.0-20190816093944-a6b53ec6cb22/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA= gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= -gopkg.in/olivere/elastic.v6 v6.2.37 h1:y1SqAL8MJvKckEOo3aZ+Ie0TDIYjrItZ9WBN3VzhoRM= -gopkg.in/olivere/elastic.v6 v6.2.37/go.mod h1:2cTT8Z+/LcArSWpCgvZqBgt3VOqXiy7v00w12Lz8bd4= 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.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/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-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= grpc.go4.org v0.0.0-20170609214715-11d0a25b4919/go.mod h1:77eQGdRu53HpSqPFJFmuJdjuHRquDANNeA4x7B8WQ9o= diff --git a/server/i18n/am.json b/server/i18n/am.json new file mode 100644 index 00000000000..a76c74535ac --- /dev/null +++ b/server/i18n/am.json @@ -0,0 +1,454 @@ +[ + { + "id": "app.scheme.save.app_error", + "translation": "ቻናሉን ማስተካከል አልተቻለም." + }, + { + "id": "app.scheme.get.app_error", + "translation": "ቻናሉን ማስተካከል አልተቻለም." + }, + { + "id": "app.reaction.save.save.app_error", + "translation": "ቻናሉን ማስተካከል አልተቻለም." + }, + { + "id": "app.emoji.delete.app_error", + "translation": "ቻናሉን ማስተካከል አልተቻለም." + }, + { + "id": "store.sql_command.save.get.app_error", + "translation": "ቻናሉን ማስተካከል አልተቻለም." + }, + { + "id": "app.channel.update.bad_id", + "translation": "ቻናሉን ማስተካከል አልተቻለም." + }, + { + "id": "app.channel.get_deleted.existing.app_error", + "translation": "ቻናሉን ማስተካከል አልተቻለም." + }, + { + "id": "store.sql_channel.get.existing.app_error", + "translation": "ቻናሉን ማስተካከል አልተቻለም." + }, + { + "id": "manaultesting.test_autolink.unable.app_error", + "translation": "ቻናሉን ማስተካከል አልተቻለም." + }, + { + "id": "ent.saml.service_disable.app_error", + "translation": "SAML 2.0 በዚህ ሰርቨር ላይ አልተዋቀረም ወይም አይሰራም." + }, + { + "id": "ent.saml.metadata.app_error", + "translation": "የአገልግሎት አቅራቢ ሜታዳታ በሚገነቡበት ጊዜ ስህተት ተከስቷል." + }, + { + "id": "ent.message_export.global_relay.create_file_in_zip.app_error", + "translation": "ቻናሉን ማስተካከል አልተቻለም." + }, + { + "id": "ent.message_export.global_relay.close_zip_file.app_error", + "translation": "ቻናሉን ማስተካከል አልተቻለም." + }, + { + "id": "ent.elasticsearch.index_user.error", + "translation": "ቻናሉን ማስተካከል አልተቻለም." + }, + { + "id": "ent.elasticsearch.index_post.error", + "translation": "ቻናሉን ማስተካከል አልተቻለም." + }, + { + "id": "ent.elasticsearch.index_channel.error", + "translation": "ቻናሉን ማስተካከል አልተቻለም." + }, + { + "id": "ent.elasticsearch.delete_user.error", + "translation": "ቻናሉን ማስተካከል አልተቻለም." + }, + { + "id": "ent.elasticsearch.delete_post.error", + "translation": "ቻናሉን ማስተካከል አልተቻለም." + }, + { + "id": "ent.elasticsearch.delete_channel.error", + "translation": "ቻናሉን ማስተካከል አልተቻለም." + }, + { + "id": "ent.compliance.csv.zip.creation.appError", + "translation": "ቻናሉን ማስተካከል አልተቻለም." + }, + { + "id": "ent.compliance.csv.warning.appError", + "translation": "ቻናሉን ማስተካከል አልተቻለም." + }, + { + "id": "ent.compliance.csv.metadata.json.zipfile.appError", + "translation": "ቻናሉን ማስተካከል አልተቻለም." + }, + { + "id": "ent.account_migration.get_saml_users_failed", + "translation": "ቻናሉን ማስተካከል አልተቻለም." + }, + { + "id": "ent.account_migration.get_all_failed", + "translation": "ቻናሉን ማስተካከል አልተቻለም." + }, + { + "id": "bleveengine.index_user.error", + "translation": "ቻናሉን ማስተካከል አልተቻለም." + }, + { + "id": "bleveengine.index_post.error", + "translation": "ቻናሉን ማስተካከል አልተቻለም." + }, + { + "id": "bleveengine.index_channel.error", + "translation": "ቻናሉን ማስተካከል አልተቻለም." + }, + { + "id": "bleveengine.delete_user.error", + "translation": "ቻናሉን ማስተካከል አልተቻለም." + }, + { + "id": "bleveengine.delete_post.error", + "translation": "ቻናሉን ማስተካከል አልተቻለም." + }, + { + "id": "bleveengine.delete_channel.error", + "translation": "ቻናሉን ማስተካከል አልተቻለም." + }, + { + "id": "app.plugin.write_file.saving.app_error", + "translation": "ሰርቲፊኬት ፋይሉን በመሰረዝ ላይ ሳለ ስህተት ተከስቷል." + }, + { + "id": "app.plugin.write_file.read.app_error", + "translation": "ሰርቲፊኬት ፋይሉን በመሰረዝ ላይ ሳለ ስህተት ተከስቷል." + }, + { + "id": "app.plugin.remove.app_error", + "translation": "ቻናሉን ማስተካከል አልተቻለም." + }, + { + "id": "app.plugin.get_plugins.app_error", + "translation": "ቻናሉን ማስተካከል አልተቻለም." + }, + { + "id": "app.plugin.delete_public_key.delete.app_error", + "translation": "ሰርቲፊኬት ፋይሉን በመሰረዝ ላይ ሳለ ስህተት ተከስቷል." + }, + { + "id": "app.plugin.deactivate.app_error", + "translation": "ቻናሉን ማስተካከል አልተቻለም." + }, + { + "id": "app.import.import_direct_channel.update_header_failed.error", + "translation": "ቻናሉን ማስተካከል አልተቻለም." + }, + { + "id": "app.channel.create_direct_channel.internal_error", + "translation": "ቻናሉን ማስተካከል አልተቻለም." + }, + { + "id": "app.channel.create_channel.internal_error", + "translation": "ቻናሉን ማስተካከል አልተቻለም." + }, + { + "id": "app.bot.permenent_delete.bad_id", + "translation": "ቻናሉን ማስተካከል አልተቻለም." + }, + { + "id": "app.bot.patchbot.internal_error", + "translation": "ቻናሉን ማስተካከል አልተቻለም." + }, + { + "id": "app.bot.getbots.internal_error", + "translation": "ቻናሉን ማስተካከል አልተቻለም." + }, + { + "id": "app.bot.getbot.internal_error", + "translation": "ቻናሉን ማስተካከል አልተቻለም." + }, + { + "id": "app.bot.createbot.internal_error", + "translation": "ቻናሉን ማስተካከል አልተቻለም." + }, + { + "id": "api.user.upload_profile_user.upload_profile.app_error", + "translation": "አይከን ምስሉን መጫን አልተቻለም." + }, + { + "id": "api.user.upload_profile_user.too_large.app_error", + "translation": "የአይከን ምስሉን መጫን አልተቻለም.ፋይሉ በጣም ትልቅ ነው." + }, + { + "id": "api.user.upload_profile_user.open.app_error", + "translation": "የምስል ፋይሉን መክፈት አልተቻለም." + }, + { + "id": "api.user.upload_profile_user.encode.app_error", + "translation": "አይከን ምስሉን መጫን አልተቻለም." + }, + { + "id": "api.user.upload_profile_user.decode.app_error", + "translation": "አይከን ምስሉን መጫን አልተቻለም." + }, + { + "id": "api.user.update_active.permissions.app_error", + "translation": "አስፈላጊው ፍቃድ የለዎትም" + }, + { + "id": "api.user.saml.not_available.app_error", + "translation": "SAML 2.0 በዚህ ሰርቨር ላይ አልተዋቀረም ወይም አይሰራም." + }, + { + "id": "api.user.create_profile_image.encode.app_error", + "translation": "አይከን ምስሉን መጫን አልተቻለም." + }, + { + "id": "api.user.create_profile_image.default_font.app_error", + "translation": "አይከን ምስሉን መጫን አልተቻለም." + }, + { + "id": "api.team.set_team_icon.too_large.app_error", + "translation": "የአይከን ምስሉን መጫን አልተቻለም.ፋይሉ በጣም ትልቅ ነው." + }, + { + "id": "api.team.set_team_icon.open.app_error", + "translation": "የምስል ፋይሉን መክፈት አልተቻለም." + }, + { + "id": "api.team.import_team.open.app_error", + "translation": "የምስል ፋይሉን መክፈት አልተቻለም." + }, + { + "id": "api.team.add_members.user_denied", + "translation": "Den här kanalen hanteras av grupper. Den här användaren tillhör inte en grupp synkad till den här kanalen." + }, + { + "id": "api.team.add_members.error", + "translation": "ቻናል አባል መጨመር ላይ ስህተት ተክስቷል." + }, + { + "id": "api.preference.update_preferences.set.app_error", + "translation": "ቻናሉን ማስተካከል አልተቻለም." + }, + { + "id": "api.preference.preferences_category.get.app_error", + "translation": "ቻናሉን ማስተካከል አልተቻለም." + }, + { + "id": "api.preference.delete_preferences.delete.app_error", + "translation": "ቻናሉን ማስተካከል አልተቻለም." + }, + { + "id": "api.post_get_post_by_id.get.app_error", + "translation": "ቻናሉን ማስተካከል አልተቻለም." + }, + { + "id": "api.post.update_post.find.app_error", + "translation": "ቻናሉን ማስተካከል አልተቻለም." + }, + { + "id": "api.license.add_license.open.app_error", + "translation": "የምስል ፋይሉን መክፈት አልተቻለም." + }, + { + "id": "api.context.permissions.app_error", + "translation": "አስፈላጊው ፍቃድ የለዎትም" + }, + { + "id": "api.command_msg.missing.app_error", + "translation": "ቻናሉን ማስተካከል አልተቻለም." + }, + { + "id": "api.command_msg.fail.app_error", + "translation": "ሰርቲፊኬት ፋይሉን በመሰረዝ ላይ ሳለ ስህተት ተከስቷል." + }, + { + "id": "api.command_leave.fail.app_error", + "translation": "ሰርቲፊኬት ፋይሉን በመሰረዝ ላይ ሳለ ስህተት ተከስቷል." + }, + { + "id": "api.command_join.missing.app_error", + "translation": "ቻናሉን ማስተካከል አልተቻለም." + }, + { + "id": "api.command_join.list.app_error", + "translation": "ሰርቲፊኬት ፋይሉን በመሰረዝ ላይ ሳለ ስህተት ተከስቷል." + }, + { + "id": "api.command_join.fail.app_error", + "translation": "ሰርቲፊኬት ፋይሉን በመሰረዝ ላይ ሳለ ስህተት ተከስቷል." + }, + { + "id": "api.command_invite.group_constrained_user_denied", + "translation": "Den här kanalen hanteras av grupper. Den här användaren tillhör inte en grupp synkad till den här kanalen." + }, + { + "id": "api.command_invite.fail.app_error", + "translation": "ሰርቲፊኬት ፋይሉን በመሰረዝ ላይ ሳለ ስህተት ተከስቷል." + }, + { + "id": "api.command_groupmsg.fail.app_error", + "translation": "ሰርቲፊኬት ፋይሉን በመሰረዝ ላይ ሳለ ስህተት ተከስቷል." + }, + { + "id": "api.command_expand_collapse.fail.app_error", + "translation": "ሰርቲፊኬት ፋይሉን በመሰረዝ ላይ ሳለ ስህተት ተከስቷል." + }, + { + "id": "api.channel.update_channel.deleted.app_error", + "translation": "ቻናሉ ተሰርዟል ወይም አርካይቭ ውስጥ ገብቷል." + }, + { + "id": "api.channel.remove_user_from_channel.app_error", + "translation": "ከዚህ አይነት ቻናል ላይ ተጠቃሚ ማስወገድ አይቻልም." + }, + { + "id": "api.channel.patch_update_channel.forbidden.app_error", + "translation": "ቻናሉን ማስተካከል አልተቻለም." + }, + { + "id": "api.channel.join_channel.permissions.app_error", + "translation": "አስፈላጊው ፍቃድ የለዎትም" + }, + { + "id": "api.channel.delete_channel.deleted.app_error", + "translation": "ቻናሉ ተሰርዟል ወይም አርካይቭ ውስጥ ገብቷል." + }, + { + "id": "api.channel.create_group.bad_user.app_error", + "translation": "ከተሰጡት ተጠቃሚዎች ውስጥ አንዱ የለም." + }, + { + "id": "api.channel.create_group.bad_size.app_error", + "translation": "የጋራ መልዕክት ቻናሎች ከ 3 ያላነሱ እና ከ 8 ያልበለጡ ተጠቃሚዎችን ብቻ መያዝ አለባቸው." + }, + { + "id": "api.channel.create_default_channels.town_square", + "translation": "ታውን ስኩዌር" + }, + { + "id": "api.channel.create_default_channels.off_topic", + "translation": "ከርዕስ ውጭ" + }, + { + "id": "api.channel.change_channel_privacy.public_to_private", + "translation": "ይህ ቻናል ወደ ግላዊ ቻናል ተቀይሯል." + }, + { + "id": "api.channel.change_channel_privacy.private_to_public", + "translation": "ይህ ቻናል ወደ ይፋዊ ቻናል የተለወጠ ስለሆነ ማንኛውም የቡድን አባል ሊቀላቀል ይችላል." + }, + { + "id": "api.channel.add_user_to_channel.type.app_error", + "translation": "ወደዚህ አይነት ቻናል ላይ ተጠቃሚ መጨመር አይቻልም." + }, + { + "id": "api.channel.add_user.to.channel.failed.deleted.app_error", + "translation": "ተጠቃሚው ከቡድኑ ስለተወገዱ ወደ ቻናሉ መጨመር አልተቻለም." + }, + { + "id": "api.channel.add_members.error", + "translation": "ቻናል አባል መጨመር ላይ ስህተት ተክስቷል." + }, + { + "id": "api.admin.upload_brand_image.too_large.app_error", + "translation": "ፋይሉ ትልቅ ስለሆነ መጫን አልተቻለም." + }, + { + "id": "api.admin.test_email.reenter_password", + "translation": "የ SMTP ሰርቨሩ፣ የፖርት ቁጥር ወይም የተጠቃሚው ስም ተቀይሯል. ግንኙነትን ለመሞከር እባክዎ የ SMTP የይለፍ ቃልዎን እንደገና ያስገቡ." + }, + { + "id": "api.admin.test_email.missing_server", + "translation": "የ SMTP ሰርቨር ያስፈልጋል." + }, + { + "id": "api.admin.test_email.body", + "translation": "በጣም አስፈላጊ የሆነው የእርስዎ ኢሜል በትክክል ማዋቀሩን ያሳያል!" + }, + { + "id": "api.admin.saml.not_available.app_error", + "translation": "SAML 2.0 በዚህ ሰርቨር ላይ አልተዋቀረም ወይም አይሰራም." + }, + { + "id": "api.admin.saml.metadata.app_error", + "translation": "የአገልግሎት አቅራቢ ሜታዳታ በሚገነቡበት ጊዜ ስህተት ተከስቷል." + }, + { + "id": "api.admin.saml.failure_save_idp_certificate_file.app_error", + "translation": "የምስል ፋይሉን መክፈት አልተቻለም." + }, + { + "id": "api.admin.remove_certificate.delete.app_error", + "translation": "ሰርቲፊኬት ፋይሉን በመሰረዝ ላይ ሳለ ስህተት ተከስቷል." + }, + { + "id": "api.admin.get_brand_image.storage.app_error", + "translation": "የምስል ማከማቻ አልተዋቀረም." + }, + { + "id": "api.admin.file_read_error", + "translation": "ስህተት ስለተፈጠረ ሎግ ፋይሉን ማንበብ አልተቻለም. " + }, + { + "id": "api.admin.add_certificate.saving.app_error", + "translation": "የምስል ፋይሉን መክፈት አልተቻለም." + }, + { + "id": "api.admin.add_certificate.open.app_error", + "translation": "የምስል ፋይሉን መክፈት አልተቻለም." + }, + { + "id": "September", + "translation": "መስከረም" + }, + { + "id": "October", + "translation": "ጥቅምት" + }, + { + "id": "November", + "translation": "ሕዳር" + }, + { + "id": "May", + "translation": "ግንቦት" + }, + { + "id": "March", + "translation": "መጋቢት" + }, + { + "id": "June", + "translation": "ሰኔ" + }, + { + "id": "July", + "translation": "ሐምሌ" + }, + { + "id": "January", + "translation": "ጥር" + }, + { + "id": "February", + "translation": "የካቲት" + }, + { + "id": "December", + "translation": "ታሕሣስ" + }, + { + "id": "August", + "translation": "ነሐሴ" + }, + { + "id": "April", + "translation": "ሚያዚያ" + } +] diff --git a/server/i18n/be.json b/server/i18n/be.json new file mode 100644 index 00000000000..a3835f4fa8f --- /dev/null +++ b/server/i18n/be.json @@ -0,0 +1,9174 @@ +[ + { + "id": "api.slackimport.slack_add_users.created", + "translation": "\nКарыстальнiкi створаны:\n" + }, + { + "id": "api.slackimport.slack_add_channels.added", + "translation": "\nДададзеныя каналы:\n" + }, + { + "id": "api.server.warn_metric.number_of_posts_2M.notification_title", + "translation": "Павышэнне Прадукцыйнасці" + }, + { + "id": "api.server.warn_metric.email_us", + "translation": "Напішыце нам" + }, + { + "id": "api.server.warn_metric.bot_response.mailto_email_header", + "translation": "Электронная пошта: {{.Email}}" + }, + { + "id": "api.server.warn_metric.bot_response.mailto_contact_header", + "translation": "Кантакт: {{.Contact}}" + }, + { + "id": "api.remote_cluster.invalid_topic.app_error", + "translation": "Няправільная тэма." + }, + { + "id": "api.remote_cluster.invalid_id.app_error", + "translation": "Няправільны ідэнтыфікатар." + }, + { + "id": "api.post.send_notifications_and_forget.push_explicit_mention", + "translation": " згадаў вас." + }, + { + "id": "api.oauth.auth_complete", + "translation": "Аўтэнтыфікацыя завершана" + }, + { + "id": "api.marshal_error", + "translation": "памылка маршалiнга" + }, + { + "id": "api.io_error", + "translation": "памылка ўводу/вываду" + }, + { + "id": "api.email_batching.send_batched_email_notification.time", + "translation": "{{.Hour}}:{{.Minute}} {{.TimeZone}}" + }, + { + "id": "api.templates.warn_metric_ack.body.contact_email_header", + "translation": "Электронная пошта: " + }, + { + "id": "api.slackimport.slack_import.notes", + "translation": "\nНататкi:\n" + }, + { + "id": "api.command_shrug.name", + "translation": "паціснуць-плячыма" + }, + { + "id": "api.command_shortcuts.name", + "translation": "цэтлікі" + }, + { + "id": "api.command_share.name", + "translation": "абагулiць-канал" + }, + { + "id": "api.command_remote.name", + "translation": "бяспечнае-злучэнне" + }, + { + "id": "api.templates.questions_footer.title", + "translation": "Пытаннi?" + }, + { + "id": "api.templates.cloud_welcome_email.subject", + "translation": "Віншуем!" + }, + { + "id": "api.command_shrug.hint", + "translation": "[паведамленне]" + }, + { + "id": "api.command_share.hint", + "translation": "[дзеянне]" + }, + { + "id": "api.command_settings.name", + "translation": "наладкi" + }, + { + "id": "api.command_search.name", + "translation": "шукаць" + }, + { + "id": "api.command_search.hint", + "translation": "[тэкст]" + }, + { + "id": "api.command_remove.name", + "translation": "выдалiць" + }, + { + "id": "api.command_remove.hint", + "translation": "@[імя карыстальніка]" + }, + { + "id": "api.command_remote.hint", + "translation": "[дзеянне]" + }, + { + "id": "api.command_open.name", + "translation": "адкрыць" + }, + { + "id": "api.command_online.name", + "translation": "анлайн" + }, + { + "id": "api.command_offline.name", + "translation": "афлайн" + }, + { + "id": "api.command_mute.name", + "translation": "не апавяшчаць" + }, + { + "id": "api.command_mute.hint", + "translation": "~[канал]" + }, + { + "id": "api.command_msg.name", + "translation": "Паведамленне" + }, + { + "id": "api.command_me.name", + "translation": "я" + }, + { + "id": "api.command_me.hint", + "translation": "[паведамленне]" + }, + { + "id": "api.command_logout.name", + "translation": "выхад з сістэмы" + }, + { + "id": "api.command_leave.name", + "translation": "пакінуць" + }, + { + "id": "api.command_kick.name", + "translation": "выкінуць" + }, + { + "id": "api.command_join.name", + "translation": "далучыцца" + }, + { + "id": "api.command_join.hint", + "translation": "~[канал]" + }, + { + "id": "api.command_invite.name", + "translation": "запрасiць" + }, + { + "id": "api.command_help.name", + "translation": "дапамога" + }, + { + "id": "api.command_groupmsg.name", + "translation": "паведамленне" + }, + { + "id": "api.command_expand.name", + "translation": "разгарнуць" + }, + { + "id": "api.command_echo.name", + "translation": "рэха" + }, + { + "id": "api.command_dnd.name", + "translation": "не турбаваць" + }, + { + "id": "api.command_custom_status.name", + "translation": "стан" + }, + { + "id": "api.command_collapse.name", + "translation": "згарнуць" + }, + { + "id": "api.command_code.name", + "translation": "код" + }, + { + "id": "api.command_code.hint", + "translation": "[тэкст]" + }, + { + "id": "api.command_channel_rename.name", + "translation": "перайменаваць" + }, + { + "id": "api.command_channel_rename.hint", + "translation": "[тэкст]" + }, + { + "id": "api.command_channel_purpose.name", + "translation": "прызначэнне" + }, + { + "id": "api.command_channel_purpose.hint", + "translation": "[тэкст]" + }, + { + "id": "api.command_channel_header.name", + "translation": "загаловак" + }, + { + "id": "api.command_channel_header.hint", + "translation": "[тэкст]" + }, + { + "id": "api.command_away.name", + "translation": "адышоў" + }, + { + "id": "api.command.invite_people.name", + "translation": "запрасiць_людзей" + }, + { + "id": "api.channel.create_default_channels.off_topic", + "translation": "Оффтоп" + }, + { + "id": "model.incoming_hook.username.app_error", + "translation": "Няправільнае імя карыстальніка." + }, + { + "id": "model.bot.is_valid.username.app_error", + "translation": "Няправільнае імя карыстальніка." + }, + { + "id": "model.outgoing_hook.username.app_error", + "translation": "Няправільнае імя карыстальніка." + }, + { + "id": "api.incoming_webhook.invalid_username.app_error", + "translation": "Няправільнае імя карыстальніка." + }, + { + "id": "api.email_batching.send_batched_email_notification.button", + "translation": "Адкрыць Mattermost" + }, + { + "id": "api.command_remote.invite_password.help", + "translation": "Пароль запрашэння" + }, + { + "id": "api.command_remote.invitation_created", + "translation": "Запрашэнне створана." + }, + { + "id": "api.command_msg.hint", + "translation": "@[імя карыстальніка] 'паведамленне'" + }, + { + "id": "api.command_invite.hint", + "translation": "@[імя карыстальніка] ~[канал]" + }, + { + "id": "api.command_groupmsg.hint", + "translation": "@[username1],@[username2] 'паведамленне'" + }, + { + "id": "api.command.invite_people.hint", + "translation": "[name@domain.com ...]" + }, + { + "id": "api.channel.create_default_channels.town_square", + "translation": "Гарадская плошча" + }, + { + "id": "web.error.unsupported_browser.system_browser_or", + "translation": "або" + }, + { + "id": "web.error.unsupported_browser.browser_title.safari", + "translation": "Safari" + }, + { + "id": "web.error.unsupported_browser.browser_title.firefox", + "translation": "Firefox" + }, + { + "id": "system.message.name", + "translation": "Сiстэма" + }, + { + "id": "error", + "translation": "Памылка" + }, + { + "id": "app.system.system_bot.bot_displayname", + "translation": "Сiстэма" + }, + { + "id": "api.templates.welcome_body.app_download_button", + "translation": "Спампаваць" + }, + { + "id": "api.templates.warn_metric_ack.body.contact_name_header", + "translation": "Кантакт: " + }, + { + "id": "api.admin.saml.failure_save_idp_certificate_file.app_error", + "translation": "Немагчыма захаваць файл сертыфiката." + }, + { + "id": "api.admin.add_certificate.saving.app_error", + "translation": "Немагчыма захаваць файл сертыфiката." + }, + { + "id": "api.admin.add_certificate.parseform.app_error", + "translation": "Памылка сінтаксічнага аналізу шматфарматнага запыту" + }, + { + "id": "api.admin.add_certificate.open.app_error", + "translation": "Немагчыма адкрыць файл сертыфiката." + }, + { + "id": "api.admin.file_read_error", + "translation": "Памылка чытання лог файла." + }, + { + "id": "api.admin.ldap.not_available.app_error", + "translation": "LDAP недаступны." + }, + { + "id": "September", + "translation": "Верасень" + }, + { + "id": "October", + "translation": "Кастрычнiк" + }, + { + "id": "November", + "translation": "Лiстапад" + }, + { + "id": "May", + "translation": "Травень" + }, + { + "id": "March", + "translation": "Сакавiк" + }, + { + "id": "June", + "translation": "Чэрвень" + }, + { + "id": "July", + "translation": "Лiпень" + }, + { + "id": "January", + "translation": "Студзень" + }, + { + "id": "February", + "translation": "Люты" + }, + { + "id": "December", + "translation": "Снежань" + }, + { + "id": "August", + "translation": "Жнiвень" + }, + { + "id": "April", + "translation": "Красавiк" + }, + { + "id": "api.user.complete_switch_with_oauth.blank_email.app_error", + "translation": "Пусты ліст." + }, + { + "id": "api.user.authorize_oauth_user.invalid_state.app_error", + "translation": "Недапушчальны стан" + }, + { + "id": "api.upload.upload_data.invalid_content_length", + "translation": "Недапушчальны Content-Length." + }, + { + "id": "api.templates.welcome_body.button", + "translation": "Пацвердзіць электронную пошту" + }, + { + "id": "api.templates.warn_metric_ack.body.site_url_header", + "translation": "URL сайта: " + }, + { + "id": "api.templates.warn_metric_ack.body.diagnostic_id_header", + "translation": "Дыягнастычны ідэнтыфікатар: " + }, + { + "id": "api.templates.verify_body.button", + "translation": "Пацвердзіць электронную пошту" + }, + { + "id": "api.templates.reset_body.button", + "translation": "Скінуць Пароль" + }, + { + "id": "api.templates.post_body.button", + "translation": "Прагляд Паведамлення" + }, + { + "id": "api.templates.payment_failed_no_card.button", + "translation": "Аплаціце зараз" + }, + { + "id": "api.templates.payment_failed.title", + "translation": "Няўдалы Плацёж" + }, + { + "id": "api.templates.license_up_for_renewal_renew_now", + "translation": "Падоўжыць зараз" + }, + { + "id": "api.templates.invite_body_footer.learn_more", + "translation": "Даведацца больш" + }, + { + "id": "api.templates.invite_body.button", + "translation": "Далучыцца" + }, + { + "id": "api.templates.email_organization", + "translation": "Даслаў " + }, + { + "id": "api.templates.email_info2", + "translation": "З найлепшымі пажаданнямі," + }, + { + "id": "api.templates.email_change_verify_body.button", + "translation": "Пацвердзіць электронную пошту" + }, + { + "id": "api.templates.cloud_welcome_email.button", + "translation": "Адкрыць Mattermost" + }, + { + "id": "api.templates.cloud_welcome_email.app_market_place", + "translation": "Крама прыкладанняў." + }, + { + "id": "model.guest.is_valid.channels.app_error", + "translation": "Няправiльныя каналы." + }, + { + "id": "model.guest.is_valid.channel.app_error", + "translation": "Няправiльны канал." + }, + { + "id": "model.compliance.is_valid.id.app_error", + "translation": "Няправільны ідэнтыфікатар." + }, + { + "id": "model.compliance.is_valid.desc.app_error", + "translation": "Няправільнае апісанне." + }, + { + "id": "model.command.is_valid.url.app_error", + "translation": "Няправільны URL." + }, + { + "id": "model.command.is_valid.trigger.app_error", + "translation": "Няправільны трыгер." + }, + { + "id": "model.command.is_valid.token.app_error", + "translation": "Няправiльны токен." + }, + { + "id": "model.command.is_valid.method.app_error", + "translation": "Няправільны метад." + }, + { + "id": "model.command.is_valid.id.app_error", + "translation": "Няправільны ідэнтыфікатар." + }, + { + "id": "model.command.is_valid.display_name.app_error", + "translation": "Несапраўдны загаловак." + }, + { + "id": "model.command.is_valid.description.app_error", + "translation": "Няправільнае апісанне." + }, + { + "id": "model.command.is_valid.autocomplete_data.app_error", + "translation": "Несапраўдныя дадзеныя аўтазапаўнення" + }, + { + "id": "model.cluster.is_valid.id.app_error", + "translation": "Няправільны ідэнтыфікатар." + }, + { + "id": "model.channel.is_valid.type.app_error", + "translation": "Няправiльны тып." + }, + { + "id": "model.channel.is_valid.purpose.app_error", + "translation": "Несапраўдная мэта." + }, + { + "id": "model.channel.is_valid.id.app_error", + "translation": "Няправільны ідэнтыфікатар." + }, + { + "id": "model.channel.is_valid.header.app_error", + "translation": "Няправільны загаловак." + }, + { + "id": "model.bot.is_valid.description.app_error", + "translation": "Няправільнае апісанне." + }, + { + "id": "model.authorize.is_valid.state.app_error", + "translation": "Недапушчальны стан." + }, + { + "id": "model.authorize.is_valid.scope.app_error", + "translation": "Несапраўдная сфера прымянення." + }, + { + "id": "ent.ldap.do_login.invalid_password.app_error", + "translation": "Няправільны пароль." + }, + { + "id": "app.update_error", + "translation": "памылка абнаўлення" + }, + { + "id": "app.system.warn_metric.bot_displayname", + "translation": "Mattermost Дарадца" + }, + { + "id": "app.select_error", + "translation": "памылка выбару" + }, + { + "id": "app.notification.body.dm.time", + "translation": "{{.Hour}}:{{.Minute}} {{.TimeZone}}" + }, + { + "id": "app.insert_error", + "translation": "памылка ўстаўкі" + }, + { + "id": "app.command_webhook.try_use.invalid", + "translation": "Няправільны вэбхук." + }, + { + "id": "api.post.create_post.root_id.app_error", + "translation": "Няправільны параметр RootId." + }, + { + "id": "api.oauth.invalid_state_token.app_error", + "translation": "Няправільны токен стану." + }, + { + "id": "api.oauth.get_access_token.missing_refresh_token.app_error", + "translation": "invalid_request: Адсутнічае refresh_token." + }, + { + "id": "api.oauth.get_access_token.missing_code.app_error", + "translation": "invalid_request: Адсутничае код." + }, + { + "id": "api.oauth.get_access_token.bad_grant.app_error", + "translation": "invalid_request: Няправільны grant_type." + }, + { + "id": "api.oauth.get_access_token.bad_client_secret.app_error", + "translation": "invalid_request: Адсутнічае client_secret." + }, + { + "id": "api.oauth.get_access_token.bad_client_id.app_error", + "translation": "invalid_request: Няправільны client_id." + }, + { + "id": "api.license.add_license.invalid.app_error", + "translation": "Няправільны файл ліцэнзіі." + }, + { + "id": "api.emoji.create.other_user.app_error", + "translation": "Няправільны ідэнтыфікатар карыстальніка." + }, + { + "id": "api.email_batching.send_batched_email_notification.messageButton", + "translation": "Праглядзець паведамленне" + }, + { + "id": "api.context.json_encoding.app_error", + "translation": "Памылка кадавання JSON." + }, + { + "id": "api.context.invitation_expired.error", + "translation": "Скончыўся тэрмін дзеяння запрашэння." + }, + { + "id": "api.context.invalid_param.app_error", + "translation": "Няправільны параметр {{. Імя}}." + }, + { + "id": "api.command_share.available_actions", + "translation": "Даступныя дзеянні: {{.Actions}}" + }, + { + "id": "api.command_remote.unknown_action", + "translation": "Невядомае дзеянне `{{.Action}}`" + }, + { + "id": "api.command_remote.name.help", + "translation": "Імя бяспечнага злучэння" + }, + { + "id": "api.command_me.desc", + "translation": "Зрабіце дзеянне" + }, + { + "id": "api.command_logout.desc", + "translation": "Выйсці з Mattermost" + }, + { + "id": "api.command_channel_rename.desc", + "translation": "Перайменаваць канал" + }, + { + "id": "api.command.invite_people.sent", + "translation": "Запрашэнні па электроннай пошце адпраўлены" + }, + { + "id": "api.channel.patch_channel_moderations.cache_invalidation.error", + "translation": "Памылка скіду кэша" + }, + { + "id": "api.back_to_app", + "translation": "Назад да {{.SiteName}}" + }, + { + "id": "api.admin.saml.set_certificate_from_metadata.invalid_content_type.app_error", + "translation": "Няправільны тып кантэнту." + }, + { + "id": "api.admin.saml.set_certificate_from_metadata.missing_content_type.app_error", + "translation": "Адсутнічае тып кантэнту." + }, + { + "id": "model.incoming_hook.display_name.app_error", + "translation": "Няправільны загаловак." + }, + { + "id": "api.admin.saml.set_certificate_from_metadata.invalid_body.app_error", + "translation": "Няправільны тэкст сэртыфіката." + }, + { + "id": "web.incoming_webhook.invalid.app_error", + "translation": "Няправільны вэбхук." + }, + { + "id": "web.error.unsupported_browser.system_browser_make_default", + "translation": "Зрабіць па-змаўчанні" + }, + { + "id": "web.error.unsupported_browser.open_system_browser.edge", + "translation": "Адкрыць Edge" + }, + { + "id": "web.error.unsupported_browser.min_os_version.windows", + "translation": "Windows 8.1+" + }, + { + "id": "web.error.unsupported_browser.min_os_version.mac", + "translation": "macOS 10.14+" + }, + { + "id": "web.error.unsupported_browser.min_browser_version.safari", + "translation": "Версія 14.1+" + }, + { + "id": "web.error.unsupported_browser.min_browser_version.firefox", + "translation": "Версія 91+" + }, + { + "id": "web.error.unsupported_browser.min_browser_version.edge", + "translation": "Версія 44+" + }, + { + "id": "web.error.unsupported_browser.min_browser_version.chrome", + "translation": "Версія 89+" + }, + { + "id": "web.error.unsupported_browser.install_guide.windows", + "translation": "Інструкцыя па ўстаноўцы" + }, + { + "id": "web.error.unsupported_browser.install_guide.mac", + "translation": "Інструкцыя па ўстаноўцы" + }, + { + "id": "web.error.unsupported_browser.browser_title.edge", + "translation": "Microsoft Edge" + }, + { + "id": "web.error.unsupported_browser.browser_title.chrome", + "translation": "Google Chrome" + }, + { + "id": "model.user.is_valid.nickname.app_error", + "translation": "Няправільны нік." + }, + { + "id": "model.user.is_valid.locale.app_error", + "translation": "Няправільная лакаль." + }, + { + "id": "model.user.is_valid.email.app_error", + "translation": "Няправільны адрас электроннай пошты." + }, + { + "id": "model.token.is_valid.size", + "translation": "Няправiльны токен." + }, + { + "id": "model.team.is_valid.type.app_error", + "translation": "Няправiльны тып." + }, + { + "id": "model.team.is_valid.name.app_error", + "translation": "Няправільнае імя." + }, + { + "id": "model.team.is_valid.id.app_error", + "translation": "Няправільны ідэнтыфікатар." + }, + { + "id": "model.team.is_valid.email.app_error", + "translation": "Няправільны адрас электроннай пошты." + }, + { + "id": "model.team.is_valid.description.app_error", + "translation": "Няправільнае апісанне." + }, + { + "id": "model.preference.is_valid.theme.app_error", + "translation": "Няправільная тэма." + }, + { + "id": "model.preference.is_valid.name.app_error", + "translation": "Няправільнае імя." + }, + { + "id": "model.preference.is_valid.category.app_error", + "translation": "Няправільная катэгорыя." + }, + { + "id": "model.post.is_valid.type.app_error", + "translation": "Няправiльны тып." + }, + { + "id": "model.post.is_valid.props.app_error", + "translation": "Няправільныя рэквізіты." + }, + { + "id": "model.post.is_valid.msg.app_error", + "translation": "Няправільнае паведамленне." + }, + { + "id": "model.post.is_valid.id.app_error", + "translation": "Няправільны ідэнтыфікатар." + }, + { + "id": "model.post.is_valid.hashtags.app_error", + "translation": "Няправільныя хэштэгі." + }, + { + "id": "model.post.is_valid.filenames.app_error", + "translation": "Няправільныя імёны файлаў." + }, + { + "id": "model.outgoing_hook.is_valid.token.app_error", + "translation": "Няправiльны токен." + }, + { + "id": "model.outgoing_hook.is_valid.id.app_error", + "translation": "Няправільны ідэнтыфікатар." + }, + { + "id": "model.outgoing_hook.is_valid.display_name.app_error", + "translation": "Няправільны загаловак." + }, + { + "id": "model.outgoing_hook.is_valid.description.app_error", + "translation": "Няправільнае апісанне." + }, + { + "id": "model.outgoing_hook.icon_url.app_error", + "translation": "Няправільны значок." + }, + { + "id": "model.oauth.is_valid.name.app_error", + "translation": "Няправільнае імя." + }, + { + "id": "model.oauth.is_valid.description.app_error", + "translation": "Няправільнае апісанне." + }, + { + "id": "model.incoming_hook.id.app_error", + "translation": "Няправільны ідэнтыфікатар." + }, + { + "id": "model.incoming_hook.description.app_error", + "translation": "Няправільнае апісанне." + }, + { + "id": "model.guest.is_valid.emails.app_error", + "translation": "Няправільныя адрасы электроннай пошты." + }, + { + "id": "model.guest.is_valid.email.app_error", + "translation": "Няправільны адрас электроннай пошты." + }, + { + "id": "app.channel.get_channels_by_ids.not_found.app_error", + "translation": "Ніводны канал не знойдзены." + }, + { + "id": "app.admin.test_email.failure", + "translation": "Падключэнне не атрымалася: {{.Error}}" + }, + { + "id": "api.websocket_handler.invalid_param.app_error", + "translation": "Няправільны параметр {{.Name}}." + }, + { + "id": "api.templates.welcome_body.subTitle1", + "translation": "Дзякуй што далучыліся " + }, + { + "id": "api.templates.welcome_body.serverURL", + "translation": "{{ .ServerURL }}." + }, + { + "id": "api.templates.warn_metric_ack.body.registered_users_header", + "translation": "Агульная Колькасць Карыстальнікаў: " + }, + { + "id": "api.templates.verify_body.subTitle1", + "translation": "Дзякуй што далучыліся " + }, + { + "id": "api.templates.verify_body.serverURL", + "translation": "{{ .ServerURL }}." + }, + { + "id": "api.templates.signin_change_email.body.method_email", + "translation": "электронная пошта і пароль" + }, + { + "id": "api.templates.reset_body.title", + "translation": "Скіньце Свой Пароль" + }, + { + "id": "api.templates.remove_expired_license.body.renew_button", + "translation": "Абнавіць Ліцэнзію" + }, + { + "id": "api.templates.invite_body_footer.title", + "translation": "Што такое Mattermost?" + }, + { + "id": "api.templates.cloud_welcome_email.info", + "translation": "Дзякуй за стварэнне " + }, + { + "id": "api.team.import_team.unknown_import_from.app_error", + "translation": "Невядомая крыніца імпарту." + }, + { + "id": "api.status.user_not_found.app_error", + "translation": "Карыстальнік ня знойдзены." + }, + { + "id": "api.server.warn_metric.email_domain.notification_title", + "translation": "Стварэнне Гасцявых Аккаунтаў" + }, + { + "id": "api.server.warn_metric.bot_response.notification_failure.body", + "translation": "Дашліце нам электронную паштоўку, калі ласка." + }, + { + "id": "api.server.warn_metric.bot_response.mailto_site_url_header", + "translation": "URL сайта: {{.SiteUrl}}" + }, + { + "id": "api.push_notification.title.collapsed_threads", + "translation": "Адказаць у {{.channelName}}" + }, + { + "id": "api.post.send_notifications_and_forget.push_image_only", + "translation": " далучыў файл." + }, + { + "id": "api.post.send_notifications_and_forget.push_general_message", + "translation": " адправіў паведамленне." + }, + { + "id": "api.post.send_notification_and_forget.push_channel_mention", + "translation": " паведаміў канал." + }, + { + "id": "api.post.create_webhook_post.creating.app_error", + "translation": "Памылка стварэння публікацыі." + }, + { + "id": "api.templates.server_inactivity_button", + "translation": "Адкрыць Mattermost" + }, + { + "id": "Playbooks", + "translation": "Сцэнары" + }, + { + "id": "Channels", + "translation": "Каналы" + }, + { + "id": "Boards", + "translation": "Дошкі" + }, + { + "id": "model.config.is_valid.saml_signature_algorithm.app_error", + "translation": "Няправільны алгарытм подпісу." + }, + { + "id": "model.config.is_valid.saml_canonical_algorithm.app_error", + "translation": "Некарэктны алгарытм кананізацыі." + }, + { + "id": "model.command_hook.root_id.app_error", + "translation": "Некарэктны каранёвы ідэнтыфікатар." + }, + { + "id": "model.command.is_valid.team_id.app_error", + "translation": "Няправільны ідэнтыфікатар каманды." + }, + { + "id": "model.command.is_valid.plugin_id.app_error", + "translation": "Няправільны ідэнтыфікатар плагіна." + }, + { + "id": "model.channel_member.is_valid.notify_level.app_error", + "translation": "Недапушчальны ўзровень апавяшчэнняў." + }, + { + "id": "model.channel_member.is_valid.channel_id.app_error", + "translation": "Няправiльны ідэнтыфікатар канала." + }, + { + "id": "model.channel.is_valid.display_name.app_error", + "translation": "Недапушчальнае імя для адлюстравання." + }, + { + "id": "model.channel.is_valid.creator_id.app_error", + "translation": "Недапушчальны ідэнтыфікатар стваральніка." + }, + { + "id": "model.bot.is_valid.update_at.app_error", + "translation": "Недапушчальны параметр 'абноўлена ў'." + }, + { + "id": "model.bot.is_valid.creator_id.app_error", + "translation": "Недапушчальны ідэнтыфікатар стваральніка." + }, + { + "id": "model.bot.is_valid.create_at.app_error", + "translation": "Недапушчальны параметр 'створана ў'." + }, + { + "id": "model.authorize.is_valid.redirect_uri.app_error", + "translation": "Недапушчальны адрас рэдырэкту." + }, + { + "id": "model.access.is_valid.redirect_uri.app_error", + "translation": "Недапушчальны адрас рэдырэкту." + }, + { + "id": "mattermost.bulletin.subject", + "translation": "Бюлетэнь бяспекі Mattermost" + }, + { + "id": "ent.ldap_id_migrate.app_error", + "translation": "немагчыма мігрыраваць." + }, + { + "id": "ent.ldap.syncronize.populate_syncables", + "translation": "памылка запаўнення сінхранізаваных" + }, + { + "id": "app.import.validate_scheme_import_data.unknown_scheme.error", + "translation": "Невядомая вобласць ахопу схемы." + }, + { + "id": "api.web_socket_router.no_action.app_error", + "translation": "Няма websocket дзеяння." + }, + { + "id": "api.server.warn_metric.mfa.notification_title", + "translation": "Забяспечыць шматфактарную аўтэнтыфікацыю" + }, + { + "id": "api.post.do_action.action_integration.app_error", + "translation": "Памылка інтэграцыі дзеянняў." + }, + { + "id": "api.post.do_action.action_id.app_error", + "translation": "Несапраўдны ідэнтыфікатар дзеяння." + }, + { + "id": "model.user.is_valid.last_name.app_error", + "translation": "Няправільная прозвішча." + }, + { + "id": "model.user.is_valid.id.app_error", + "translation": "Няправільны ідэнтыфікатар карыстальніка." + }, + { + "id": "model.bot.is_valid.user_id.app_error", + "translation": "Няправільны ідэнтыфікатар карыстальніка." + }, + { + "id": "model.outgoing_hook.is_valid.user_id.app_error", + "translation": "Няправільны ідэнтыфікатар карыстальніка." + }, + { + "id": "model.reaction.is_valid.user_id.app_error", + "translation": "Няправільны ідэнтыфікатар карыстальніка." + }, + { + "id": "model.websocket_client.connect_fail.app_error", + "translation": "Немагчыма падлучыцца да сервера WebSocket." + }, + { + "id": "model.utils.decode_json.app_error", + "translation": "немагчыма раскадаваць." + }, + { + "id": "model.user_access_token.is_valid.user_id.app_error", + "translation": "Няправільны ідэнтыфікатар карыстальніка." + }, + { + "id": "model.command_hook.user_id.app_error", + "translation": "Няправільны ідэнтыфікатар карыстальніка." + }, + { + "id": "model.preference.is_valid.value.app_error", + "translation": "Занадта доўгае значэнне." + }, + { + "id": "model.authorize.is_valid.user_id.app_error", + "translation": "Няправільны ідэнтыфікатар карыстальніка." + }, + { + "id": "app.import.validate_scheme_import_data.name_invalid.error", + "translation": "Няправільная назва схемы." + }, + { + "id": "app.import.validate_scheme_import_data.description_invalid.error", + "translation": "Няправільнае апісанне схемы." + }, + { + "id": "app.import.validate_role_import_data.name_invalid.error", + "translation": "Няправільная назва ролі." + }, + { + "id": "app.import.validate_role_import_data.description_invalid.error", + "translation": "Няправільнае апісанне ролі." + }, + { + "id": "app.import.generate_password.app_error", + "translation": "Памылка стварэння пароля." + }, + { + "id": "api.web_socket_router.bad_action.app_error", + "translation": "Невядомае дзеянне WebSocket." + }, + { + "id": "api.user.update_password.failed.app_error", + "translation": "Не атрымалася абнавіць пароль." + }, + { + "id": "api.user.check_user_mfa.bad_code.app_error", + "translation": "Няправiльны MFA токен." + }, + { + "id": "api.user.authorize_oauth_user.token_failed.app_error", + "translation": "Запыт токена не атрымался." + }, + { + "id": "api.user.authorize_oauth_user.missing.app_error", + "translation": "Адсутнічае токен доступу." + }, + { + "id": "api.user.authorize_oauth_user.bad_token.app_error", + "translation": "Памылковы тып токену." + }, + { + "id": "api.templates.email_info3", + "translation": "Каманда {{.SiteName}}" + }, + { + "id": "api.server.warn_metric.number_of_teams_5.notification_title", + "translation": "Выкарыстоўваюцца дадатковыя дазволы" + }, + { + "id": "api.server.warn_metric.number_of_channels_50.notification_title", + "translation": "Выкарыстоўваюцца дадатковыя дазволы" + }, + { + "id": "api.server.warn_metric.number_of_active_users_100.notification_title", + "translation": "Маштабаванне з Mattermost" + }, + { + "id": "api.server.warn_metric.number_of_active_users_200.notification_title", + "translation": "Маштабаванне з Mattermost" + }, + { + "id": "api.server.warn_metric.number_of_active_users_500.notification_title", + "translation": "Маштабаванне з Mattermost" + }, + { + "id": "api.server.warn_metric.number_of_active_users_300.start_trial.notification_title", + "translation": "Каналы аб'яваў толькі для чытання" + }, + { + "id": "api.server.warn_metric.bot_response.mailto_diagnostic_id_header", + "translation": "Дыягнастычны Id: {{.DiagnosticId}}" + }, + { + "id": "api.post.update_post.permissions_details.app_error", + "translation": "Ужо выдалены id={{.PostId}}." + }, + { + "id": "api.templates.invite_body.title.reminder", + "translation": "Напамінальнік" + }, + { + "id": "model.preference.is_valid.id.app_error", + "translation": "Няправільны ідэнтыфікатар карыстальніка." + }, + { + "id": "model.post.is_valid.user_id.app_error", + "translation": "Няправільны ідэнтыфікатар карыстальніка." + }, + { + "id": "model.command_hook.command_id.app_error", + "translation": "Несапраўдны ідэнтыфікатар каманды." + }, + { + "id": "model.command_hook.channel_id.app_error", + "translation": "Няправiльны ідэнтыфікатар канала." + }, + { + "id": "model.command.is_valid.user_id.app_error", + "translation": "Няправільны ідэнтыфікатар карыстальніка." + }, + { + "id": "model.incoming_hook.user_id.app_error", + "translation": "Няправільны ідэнтыфікатар карыстальніка." + }, + { + "id": "model.channel_member.is_valid.user_id.app_error", + "translation": "Няправільны ідэнтыфікатар карыстальніка." + }, + { + "id": "model.team_member.is_valid.user_id.app_error", + "translation": "Недапушчальны ідэнтыфікатар карыстальніка." + }, + { + "id": "model.authorize.is_valid.response_type.app_error", + "translation": "Памылковы тып адказу." + }, + { + "id": "model.authorize.is_valid.client_id.app_error", + "translation": "Несапраўдны ідэнтыфікатар кліента." + }, + { + "id": "model.authorize.is_valid.auth_code.app_error", + "translation": "Несапраўдны код аўтарызацыі." + }, + { + "id": "model.access.is_valid.user_id.app_error", + "translation": "Няправільны ідэнтыфікатар карыстальніка." + }, + { + "id": "model.access.is_valid.refresh_token.app_error", + "translation": "Несапраўдны токен абнаўлення." + }, + { + "id": "model.access.is_valid.client_id.app_error", + "translation": "Несапраўдны ідэнтыфікатар кліента." + }, + { + "id": "model.access.is_valid.access_token.app_error", + "translation": "Несапраўдны токен доступу." + }, + { + "id": "mfa.validate_token.authenticate.app_error", + "translation": "Няправiльны MFA токен." + }, + { + "id": "mfa.activate.bad_token.app_error", + "translation": "Няправiльны MFA токен." + }, + { + "id": "ent.ldap.validate_filter.app_error", + "translation": "Няправільны фільтар AD/LDAP." + }, + { + "id": "ent.ldap.syncronize.get_all_groups.app_error", + "translation": "памылка атрымання груп." + }, + { + "id": "ent.elasticsearch.test_config.indexing_disabled.error", + "translation": "Elasticsearch адключаны." + }, + { + "id": "ent.data_retention.policies.invalid_policy", + "translation": "Няправільная палітыка." + }, + { + "id": "ent.api.post.send_notifications_and_forget.push_image_only", + "translation": " далучыў файл." + }, + { + "id": "app.post.search.app_error", + "translation": "Памылка пошуку паведамленняў" + }, + { + "id": "app.plugin.filesystem.app_error", + "translation": "Адбылася памылка файлавай сістэмы." + }, + { + "id": "app.notification.footer.infoLogin", + "translation": "Вайсці у Mattermost" + }, + { + "id": "app.license.generate_renewal_token.no_license", + "translation": "Няма ліцензіі" + }, + { + "id": "app.import.validate_user_import_data.profile_image.error", + "translation": "Няправільны відарыс профілю." + }, + { + "id": "model.config.is_valid.elastic_search.bulk_indexing_batch_size.app_error", + "translation": "Памер пакета масавага індэксавання Elasticsearch павінен быць не менш за {{.BatchSize}}." + }, + { + "id": "model.config.is_valid.bleve_search.bulk_indexing_batch_size.app_error", + "translation": "Памер пакета масавага індэксавання Bleve павінен быць не менш за {{.BatchSize}}." + }, + { + "id": "app.channel.get_file_count.app_error", + "translation": "Не атрымалася атрымаць колькасць файлаў для канала" + }, + { + "id": "model.oauth.is_valid.mattermost_app_id.app_error", + "translation": "Максімальная даўжыня MattermostAppID – 32 знака." + }, + { + "id": "app.prepackged-plugin.invalid_version.app_error", + "translation": "Папярэдне спакаваная версія плагіна не можа быць прааналізавана." + }, + { + "id": "api.team.invite_members.unable_to_send_email_with_defaults.app_error", + "translation": "SMTP не настроены ў Сістэмнай Кансолі" + }, + { + "id": "api.team.invite_members.unable_to_send_email.app_error", + "translation": "Памылка пры адпраўцы ліста" + }, + { + "id": "app.job.get_all_jobs_by_type_and_status.app_error", + "translation": "Не атрымалася атрымаць усе заданні па тыпе і статуце." + }, + { + "id": "api.templates.server_inactivity_title", + "translation": "Адкрыйце для сябе падвышаную прадукцыйнасць з дапамогай гэтых выдатных функцый" + }, + { + "id": "api.templates.server_inactivity_subtitle", + "translation": "Прывітанне, {{.Name}}, мы заўважылі, што Ваш сервер Mattermost збірае крыху пылу. Зірніце на некаторыя функцыі, якія могуць аблегчыць працоўную нагрузку ў Вашай камандзе." + }, + { + "id": "api.templates.server_inactivity_subject", + "translation": "ПРЫВІТАННЕ! Адкрыйце Mattermost, каб павысіць прадуктыўнасць вашай каманды!" + }, + { + "id": "api.templates.server_inactivity_info_bullet2", + "translation": "Кіруйце задачамі з дапамогай " + }, + { + "id": "api.templates.server_inactivity_info_bullet1", + "translation": "Кіраванне працоўным працэсам з " + }, + { + "id": "api.templates.server_inactivity_info_bullet", + "translation": "Гасцявы доступ да названага " + }, + { + "id": "api.templates.server_inactivity_info", + "translation": "Прыходзьце і праверце гэта!" + }, + { + "id": "app.member_count", + "translation": "памылка пры атрыманні колькасці ўдзельнікаў" + }, + { + "id": "app.group.crud_permission", + "translation": "Немагчыма выканаць аперацыю для гэтага тыпу крыніцы." + }, + { + "id": "app.custom_group.unique_name", + "translation": "імя групы не ўнікальна" + }, + { + "id": "api.license_error", + "translation": "для канчатковай кропкі API патрабуецца ліцэнзія" + }, + { + "id": "api.custom_groups.no_remote_id", + "translation": "remote_id павінен быць пустым для карыстацкай групы" + }, + { + "id": "api.custom_groups.must_be_referenceable", + "translation": "allow_reference павінна быць 'true' для карыстацкіх груп" + }, + { + "id": "api.custom_groups.license_error", + "translation": "не ліцэнзавана для карыстацкіх груп" + }, + { + "id": "api.custom_groups.feature_disabled", + "translation": "функцыя карыстацкіх груп адключана" + }, + { + "id": "api.custom_groups.count_err", + "translation": "памылка падліку груп" + }, + { + "id": "app.system.get_onboarding_request.app_error", + "translation": "Не ўдалося атрымаць статус завяршэння рэгістрацыі." + }, + { + "id": "api.error_set_first_admin_complete_setup", + "translation": "Памылка пры спробе захаваць першую поўную настройку адміністратара ў краме." + }, + { + "id": "api.error_get_first_admin_complete_setup", + "translation": "Памылка пры спробе атрымаць першую завершаную настройку адміністратара з крамы." + }, + { + "id": "model.emoji.system_emoji_name.app_error", + "translation": "Імя канфліктуе з існуючым сістэмным імем эмодзі." + }, + { + "id": "app.system.complete_onboarding_request.no_first_user", + "translation": "Падлучэнне можа быць выканана толькі Сістэмным Адміністратарам." + }, + { + "id": "app.system.complete_onboarding_request.app_error", + "translation": "Не ўдалося расшыфраваць поўны запыт на падлучэнне." + }, + { + "id": "api.user.authorize_oauth_user.saml_response_too_long.app_error", + "translation": "Адказ SAML занадта доўгі" + }, + { + "id": "api.license.request_renewal_link.cannot_renew_on_cws", + "translation": "Падаўжэнне гэтай ліцэнзіі на партале немагчыма" + }, + { + "id": "api.post.posts_by_ids.invalid_body.request_error", + "translation": "Колькасць атрыманых ID паведамленняў перавышае максімальны памер {{.MaxLength}}" + }, + { + "id": "api.user.view_archived_channels.get_users_in_channel.app_error", + "translation": "Не ўдаецца атрымаць карыстальнікаў для архіўнага канала" + }, + { + "id": "api.user.view_archived_channels.get_posts_for_channel.app_error", + "translation": "Не ўдаецца атрымаць паведамленні для архіўнага канала" + }, + { + "id": "app.session.extend_session_expiry.app_error", + "translation": "Немагчыма павялічыць працягласць сеансу" + }, + { + "id": "app.user.get_thread_count_for_user.app_error", + "translation": "Нам не ўдалося атрымаць колькасць абмеркаванняў для карыстальніка." + }, + { + "id": "api.custom_status.set_custom_statuses.update.app_error", + "translation": "Не атрымалася абнавіць карыстацкі статус. Калі ласка, дадайце эмоджы ці уласны тэкст ці абодва." + }, + { + "id": "app.role.get_all.app_error", + "translation": "Немагчыма атрымаць усе ролі." + }, + { + "id": "ent.elasticsearch.getAllTeamMembers.error", + "translation": "Не ўдалося атрымаць спіс усіх удзельнікаў каманды" + }, + { + "id": "ent.elasticsearch.getAllChannelMembers.error", + "translation": "Не ўдалося атрымаць спіс усіх удзельнікаў канала" + }, + { + "id": "api.user.create_user.bad_token_email_data.app_error", + "translation": "Адрас электроннай пошты ў токене не супадае з адрасам у інфармацыі карыстальніка." + }, + { + "id": "model.user.is_valid.roles_limit.app_error", + "translation": "Недапушчальныя ролі карыстальнікаў даўжэй {{.Limit}} сімвалаў." + }, + { + "id": "model.team_member.is_valid.roles_limit.app_error", + "translation": "Недапушчальныя ролі ўдзельніка каманды даўжэй {{.Limit}} сімвалаў." + }, + { + "id": "model.session.is_valid.user_id.app_error", + "translation": "Недапушчальнае поле UserId для сеанса." + }, + { + "id": "model.session.is_valid.roles_limit.app_error", + "translation": "Недапушчальныя ролі сеансу даўжэй {{.Limit}} сімвалаў." + }, + { + "id": "model.session.is_valid.id.app_error", + "translation": "Недапушчальнае поле ID сеанса." + }, + { + "id": "model.session.is_valid.create_at.app_error", + "translation": "Недапушчальнае поле CreateAt для сеанса." + }, + { + "id": "model.channel_member.is_valid.roles_limit.app_error", + "translation": "Недапушчальныя ролі ўдзельнікаў канала даўжэй {{.Limit}} сімвалаў." + }, + { + "id": "app.notification.body.thread_gm.subTitle", + "translation": "Пакуль Вас не было, {{.SenderName}} адказаў на абмеркаванне ў Вашай групе." + }, + { + "id": "app.notification.body.thread_dm.subTitle", + "translation": "Пакуль Вас не было, {{.SenderName}} адказаў на абмеркаванне ў Вашым асабістым паведамленні." + }, + { + "id": "app.notification.body.thread_channel_full.subTitle", + "translation": "Пакуль Вас не было, {{.SenderName}} адказаў у абмеркаванні, за якім Вы сачыце на {{.ChannelName}}." + }, + { + "id": "app.notification.body.thread_channel.subTitle", + "translation": "Пакуль Вас не было, {{.SenderName}} адказаў у абмеркаванні, за якім Вы сачыце." + }, + { + "id": "api.push_notification.title.collapsed_threads_dm", + "translation": "Адказаць у асабістым паведамленні" + }, + { + "id": "api.post.send_notification_and_forget.push_comment_on_crt_thread_dm", + "translation": " адказаў у абмеркаванні." + }, + { + "id": "app.post.marshal.app_error", + "translation": "Не ўдалося ўпарадкаваць паведамленне." + }, + { + "id": "api.unmarshal_error", + "translation": "Не ўдалося ўпарадкаваць." + }, + { + "id": "web.incoming_webhook.user.app_error", + "translation": "Не ўдалося знайсці карыстальніка." + }, + { + "id": "web.incoming_webhook.text.app_error", + "translation": "Тэкст не зададзены." + }, + { + "id": "web.incoming_webhook.split_props_length.app_error", + "translation": "Немагчыма падзяліць рэквізіты вэбхука на часткі сімвалаў па {{.Max}}." + }, + { + "id": "web.incoming_webhook.permissions.app_error", + "translation": "Неадпаведныя правы канала." + }, + { + "id": "web.incoming_webhook.parse.app_error", + "translation": "Немагчыма разабраць уваходныя дадзеныя." + }, + { + "id": "web.incoming_webhook.disabled.app_error", + "translation": "Уваходныя вэбхукі адключаныя сістэмным адміністратарам." + }, + { + "id": "web.incoming_webhook.channel_locked.app_error", + "translation": "Гэтаму вэбхуку не дазволена публікаваць паведамленні на запытаным канале." + }, + { + "id": "web.incoming_webhook.channel.app_error", + "translation": "Не ўдалося знайсці канал." + }, + { + "id": "web.get_access_token.internal_saving.app_error", + "translation": "Немагчыма абнавіць дадзеныя доступу карыстальніка." + }, + { + "id": "web.error.unsupported_browser.no_longer_support_version", + "translation": "Гэтая вэрсія вашага браўзэра больш не падтрымліваецца Mattermost" + }, + { + "id": "web.error.unsupported_browser.no_longer_support", + "translation": "Гэты браўзэр больш не падтрымліваецца Mattermost" + }, + { + "id": "web.error.unsupported_browser.learn_more", + "translation": "Падрабязнасці аб падтрымліваемых браўзэрах." + }, + { + "id": "web.error.unsupported_browser.download_the_app", + "translation": "Спампаваць дадатак" + }, + { + "id": "web.error.unsupported_browser.download_app_or_upgrade_browser", + "translation": "Загрузіце дадатак Mattermost або выкарыстоўвайце падтрымліваемы браўзэр для лепшай працы." + }, + { + "id": "web.error.unsupported_browser.download", + "translation": "Спампаваць дадатак" + }, + { + "id": "web.error.unsupported_browser.browser_get_latest.safari", + "translation": "Усталюйце апошнюю версію браўзэра Safari" + }, + { + "id": "web.error.unsupported_browser.browser_get_latest.firefox", + "translation": "Усталюйце апошнюю версію браўзэра Firefox" + }, + { + "id": "web.error.unsupported_browser.browser_get_latest.chrome", + "translation": "Усталюйце апошнюю версію браўзэра Chrome" + }, + { + "id": "web.command_webhook.parse.app_error", + "translation": "Немагчыма разабраць уваходныя дадзеныя." + }, + { + "id": "web.command_webhook.command.app_error", + "translation": "Не ўдалося знайсці каманду." + }, + { + "id": "store.sql_user.get_for_login.app_error", + "translation": "Не ўдалося знайсці сапраўдны ўліковы запіс з адпаведнымі ўліковымі дадзенымі. Гэтая каманда можа патрабаваць запрашэння ад уладальніка каманды для далучэння." + }, + { + "id": "store.sql_team.save_member.exists.app_error", + "translation": "Удзельнік каманды з такім ідэнтыфікатарам ужо існуе." + }, + { + "id": "store.sql_post.search.disabled", + "translation": "Пошук на гэтым сэрвэры адключаны. Калі ласка, звярніцеся да адміністратара." + }, + { + "id": "store.sql_file_info.search.disabled", + "translation": "Пошук файлаў на гэтым сэрвэры адключаны. Калі ласка, звярніцеся да адміністратара." + }, + { + "id": "store.sql_command.update.missing.app_error", + "translation": "Каманда не існуе." + }, + { + "id": "store.sql_command.save.get.app_error", + "translation": "Немагчыма атрымаць каманду." + }, + { + "id": "store.sql_command.get.missing.app_error", + "translation": "Каманда не існуе." + }, + { + "id": "store.sql_channel.save_direct_channel.not_direct.app_error", + "translation": "Спроба стварыць канал, які не з'яўляецца асабістым з выкарыстаннем SaveDirectChannel." + }, + { + "id": "store.sql_channel.save_channel.limit.app_error", + "translation": "Вы дасягнулі мяжы колькасці дазволеных каналаў." + }, + { + "id": "store.sql_channel.save_channel.exists.app_error", + "translation": "Канал з такім імем ужо існуе ў гэтай камандзе." + }, + { + "id": "store.sql_channel.save_channel.existing.app_error", + "translation": "Неабходна выклікаць абнаўленне для існуючага канала." + }, + { + "id": "store.sql_channel.save.direct_channel.app_error", + "translation": "Выкарыстоўвайце SaveDirectChannel для стварэння канала асабістых паведамленняў." + }, + { + "id": "store.sql_channel.save.archived_channel.app_error", + "translation": "Вы не можаце рэдагаваць архіўны канал." + }, + { + "id": "store.sql_channel.get.existing.app_error", + "translation": "Не ўдалося знайсці існуючы канал." + }, + { + "id": "store.sql_bot.get.missing.app_error", + "translation": "Бот не існуе." + }, + { + "id": "sharedchannel.permalink.not_found", + "translation": "Гэты пост змяшчае спасылкі на іншыя каналы, якія могуць быць нябачныя для карыстальнікаў іншых сайтаў." + }, + { + "id": "sharedchannel.cannot_deliver_post", + "translation": "Адно ці некалькі паведамленняў не могуць быць дастаўлены на выдалены сайт {{.Remote}}, бо ён у афлайне. Паведамленні будуць дастаўлены, калі сайт будзе анлайн." + }, + { + "id": "searchengine.bleve.disabled.error", + "translation": "Памылка ачысткі індэксаў Bleve: рухавічок адключаны" + }, + { + "id": "plugin_api.send_mail.missing_to", + "translation": "Адсутнічае адрас \"Каму\"." + }, + { + "id": "plugin_api.send_mail.missing_subject", + "translation": "Адсутнічае тэма email-ліста." + }, + { + "id": "plugin_api.send_mail.missing_htmlbody", + "translation": "Адсутнічае HTML Body." + }, + { + "id": "plugin_api.get_file_link.no_post.app_error", + "translation": "Не ўдалося атрымаць публічную спасылку для файла. Файл павінен быць прымацаваны да паведамлення, якое можа быць прачытана бягучым карыстальнікам." + }, + { + "id": "plugin_api.get_file_link.disabled.app_error", + "translation": "Публічныя спасылкі адключаныя." + }, + { + "id": "plugin_api.bot_cant_create_bot", + "translation": "Уліковы запіс робата не можа стварыць іншы ўліковы запіс робата." + }, + { + "id": "plugin.api.update_user_status.bad_status", + "translation": "Немагчыма ўсталяваць статут карыстача. Невядомы статус карыстальніка." + }, + { + "id": "plugin.api.get_users_in_channel", + "translation": "Не ўдалося атрымаць спіс карыстальнікаў, некарэктны крытэрый сартавання." + }, + { + "id": "oauth.gitlab.tos.error", + "translation": "Умовы прадастаўлення паслуг GitLab абноўлены. Калі ласка, зайдзіце на gitlab.com, каб прыняць іх, а затым паспрабуйце зноў увайсці ў Mattermost." + }, + { + "id": "model.user_access_token.is_valid.token.app_error", + "translation": "Несапраўдны токен доступу." + }, + { + "id": "model.user_access_token.is_valid.id.app_error", + "translation": "Недапушчальнае значэнне для ідэнтыфікатара." + }, + { + "id": "model.user_access_token.is_valid.description.app_error", + "translation": "Няправільнае апісанне, павінна быць не больш за 255 сімвалаў." + }, + { + "id": "model.user.is_valid.username.app_error", + "translation": "Username павінен быць з ліста і спіс паміж 3 і 22 characters including numbers, lowercase letters, and the symbols \".\", \"-\", and \"_\"." + }, + { + "id": "model.user.is_valid.update_at.app_error", + "translation": "\"Абноўлена ў\" павінна быць карэктным часам." + }, + { + "id": "model.user.is_valid.pwd_uppercase_symbol.app_error", + "translation": "Ваш пароль павінен змяшчаць як мінімум {{.Min}} сімвалаў, уключаючы прынамсі адну загалоўную літару і адзін спецзнак (напр., \"~!@#$%^&*()\")." + }, + { + "id": "model.user.is_valid.pwd_uppercase_number_symbol.app_error", + "translation": "Ваш пароль павінен змяшчаць як мінімум {{.Min}} сімвалаў, уключаючы прынамсі адну загалоўную літару, адну лічбу і адзін спецзнак (напр., \"~!@#$%^&*()\")." + }, + { + "id": "model.user.is_valid.pwd_uppercase_number.app_error", + "translation": "Ваш пароль павінен утрымоўваць прынамсі {{.Min}} знакаў, уключаючы прынамсі адну загалоўную літару і адну лічбу." + }, + { + "id": "model.user.is_valid.pwd_uppercase.app_error", + "translation": "Ваш пароль павінен утрымоўваць прынамсі {{.Min}} знакаў, уключаючы прынамсі адну загалоўную літару." + }, + { + "id": "model.user.is_valid.pwd_symbol.app_error", + "translation": "Ваш пароль павінен змяшчаць як мінімум {{.Min}} сімвалаў, у тым ліку па меншай меры адзін спецзнак (напр., \"~!@#$%^&*()\")." + }, + { + "id": "model.user.is_valid.pwd_number_symbol.app_error", + "translation": "Ваш пароль павінен змяшчаць як мінімум {{.Min}} сімвалаў, уключаючы па меншай меры адну лічбу і адзін спецзнак (напр., \"~!@#$%^&*()\")." + }, + { + "id": "model.user.is_valid.pwd_number.app_error", + "translation": "Ваш пароль павінен змяшчаць як мінімум {{.Min}} сімвалаў, прынамсі адзін з якіх павінен быць лічбай." + }, + { + "id": "model.user.is_valid.pwd_lowercase_uppercase_symbol.app_error", + "translation": "Ваш пароль павінен утрымоўваць прынамсі {{.Min}} знакаў, а гэтак жа адну літару ў ніжнім рэгістры, адну літару ў верхнім рэгістры, адну лічбу і спец.знак (\"~!@#$%^&*()\" )." + }, + { + "id": "model.user.is_valid.pwd_lowercase_uppercase_number_symbol.app_error", + "translation": "Ваш пароль павінен утрымоўваць прынамсі {{.Min}} знакаў, а гэтак жа адну літару ў ніжнім рэгістры, адну літару верхнім рэгістры, адну лічбу і спец.знак (\"~!@#$%^&*()\") ." + }, + { + "id": "model.user.is_valid.pwd_lowercase_uppercase_number.app_error", + "translation": "Ваш пароль павінен утрымоўваць прынамсі {{.Min}} знакаў, а гэтак жа адну літару ў верхнім рэгістры і адну лічбу." + }, + { + "id": "model.user.is_valid.pwd_lowercase_uppercase.app_error", + "translation": "Ваш пароль павінен утрымоўваць прынамсі {{.Min}} знакаў, а гэтак жа адну літару ў ніжнім і верхнім рэгістры." + }, + { + "id": "model.user.is_valid.pwd_lowercase_symbol.app_error", + "translation": "Ваш пароль павінен утрымоўваць прынамсі {{.Min}} знакаў, а гэтак жа адну літару ў ніжнім рэгістры і адзін спец.знак (\"~!@#$%^&*()\")." + }, + { + "id": "model.user.is_valid.pwd_lowercase_number_symbol.app_error", + "translation": "Ваш пароль павінен утрымоўваць прынамсі {{.Min}} знакаў, а гэтак жа адну літару ў ніжнім рэгістры, адну лічбу і адзін спец.знак (\"~!@#$%^&*()\")." + }, + { + "id": "model.user.is_valid.pwd_lowercase_number.app_error", + "translation": "Ваш пароль павінен утрымоўваць прынамсі {{.Min}} знакаў, а гэтак жа адну літару ў ніжнім рэгістры і адну лічбу." + }, + { + "id": "model.user.is_valid.pwd_lowercase.app_error", + "translation": "Ваш пароль павінен утрымоўваць прынамсі {{.Min}} знакаў, а гэтак жа адну літару ў ніжнім рэгістры." + }, + { + "id": "model.user.is_valid.pwd.app_error", + "translation": "Ваш пароль павінен утрымоўваць прынамсі {{.Min}} знакаў." + }, + { + "id": "model.user.is_valid.position.app_error", + "translation": "Няслушная пазіцыя: павінна быць не больш за 128 сімвалаў." + }, + { + "id": "model.user.is_valid.password_limit.app_error", + "translation": "Немагчыма ўсталяваць пароль даўжынёй больш за 72 сімвалаў у сувязі з абмежаваннямі bcrypt." + }, + { + "id": "model.user.is_valid.marshal.app_error", + "translation": "Не атрымалася закадаваць поле ў JSON" + }, + { + "id": "model.user.is_valid.first_name.app_error", + "translation": "Недапушчальнае імя." + }, + { + "id": "model.user.is_valid.create_at.app_error", + "translation": "\"Створана ў\" павінна быць карэктным часам." + }, + { + "id": "model.user.is_valid.auth_data_type.app_error", + "translation": "Некарэктны карыстальнік, дадзеныя аўтарызацыі павінны быць устаноўлены ў тыпе аўтарызацыі." + }, + { + "id": "model.user.is_valid.auth_data_pwd.app_error", + "translation": "Некарэктны карыстач, пароль і дадзеныя аўтарызацыі не могуць быць усталяваныя адначасова." + }, + { + "id": "model.user.is_valid.auth_data.app_error", + "translation": "Недапушчальныя дадзеныя аўтарызацыі." + }, + { + "id": "model.upload_session.is_valid.user_id.app_error", + "translation": "Недапушчальнае значэнне для UserId" + }, + { + "id": "model.upload_session.is_valid.type.app_error", + "translation": "Недапушчальнае значэнне для Type" + }, + { + "id": "model.upload_session.is_valid.path.app_error", + "translation": "Недапушчальнае значэнне для Path" + }, + { + "id": "model.upload_session.is_valid.id.app_error", + "translation": "Недапушчальнае значэнне для Id" + }, + { + "id": "model.upload_session.is_valid.filename.app_error", + "translation": "Некарэктнае значэнне для Filename" + }, + { + "id": "model.upload_session.is_valid.file_size.app_error", + "translation": "Некарэктнае значэнне для FileSize" + }, + { + "id": "model.upload_session.is_valid.file_offset.app_error", + "translation": "Некарэктнае значэнне для FileOffset" + }, + { + "id": "model.upload_session.is_valid.create_at.app_error", + "translation": "Недапушчальнае значэнне для CreateAt" + }, + { + "id": "model.upload_session.is_valid.channel_id.app_error", + "translation": "Некарэктнае значэнне для ChannelId." + }, + { + "id": "model.token.is_valid.expiry", + "translation": "Недапушчальны тэрмін дзеяння токена" + }, + { + "id": "model.team_member.is_valid.team_id.app_error", + "translation": "Недапушчальны ідэнтыфікатар каманды." + }, + { + "id": "model.team.is_valid.url.app_error", + "translation": "Недапушчальны ідэнтыфікатар URL." + }, + { + "id": "model.team.is_valid.update_at.app_error", + "translation": "\"Абноўлена ў\" павінна быць карэктным часам." + }, + { + "id": "model.team.is_valid.reserved.app_error", + "translation": "Гэты URL-адрас недаступны. Калі ласка, увядзіце іншы." + }, + { + "id": "model.team.is_valid.invite_id.app_error", + "translation": "Недапушчальны ідэнтыфікатар запрашэння." + }, + { + "id": "model.team.is_valid.domains.app_error", + "translation": "Недапушчальныя дазволеныя дамены." + }, + { + "id": "model.team.is_valid.create_at.app_error", + "translation": "\"Створана ў\" павінна быць карэктным часам." + }, + { + "id": "model.team.is_valid.company.app_error", + "translation": "Недапушчальнае імя кампаніі." + }, + { + "id": "model.team.is_valid.characters.app_error", + "translation": "Імя павінна складацца з 2 ці больш малых літар і лічбаў." + }, + { + "id": "model.search_params_list.is_valid.include_deleted_channels.app_error", + "translation": "Усе параметры IncludeDeletedChannels павінны мець аднолькавае значэнне." + }, + { + "id": "model.reaction.is_valid.update_at.app_error", + "translation": "\"Абнавіць у\" павінна быць карэктным часам." + }, + { + "id": "model.reaction.is_valid.post_id.app_error", + "translation": "Недапушчальны ідэнтыфікатар паведамлення." + }, + { + "id": "model.reaction.is_valid.emoji_name.app_error", + "translation": "Недапушчальная назва эмоджы." + }, + { + "id": "model.reaction.is_valid.create_at.app_error", + "translation": "\"Створана ў\" павінна быць карэктным часам." + }, + { + "id": "model.post.is_valid.update_at.app_error", + "translation": "\"Абноўлена ў\" павінна быць карэктным часам." + }, + { + "id": "model.post.is_valid.root_id.app_error", + "translation": "Недапушчальны каранёвы ідэнтыфікатар." + }, + { + "id": "model.post.is_valid.original_id.app_error", + "translation": "Недапушчальны ідэнтыфікатар арыгінала." + }, + { + "id": "model.post.is_valid.file_ids.app_error", + "translation": "Няправільныя ID файлаў. Звярніце ўвагу, што загрузка абмежавана максімум 10 файламі. Калі ласка, выкарыстайце дадатковыя паведамленні для адпраўкі большай колькасці файлаў." + }, + { + "id": "model.post.is_valid.create_at.app_error", + "translation": "\"Створана ў\" павінна быць карэктным часам." + }, + { + "id": "model.post.is_valid.channel_id.app_error", + "translation": "Недапушчальны ідэнтыфікатар канала." + }, + { + "id": "model.post.channel_notifications_disabled_in_channel.message", + "translation": "Апавяшчэнні канала адключаныя ў {{.ChannelName}}. {{.Mention}} не выклікае ніякіх апавяшчэнняў." + }, + { + "id": "model.plugin_kvset_options.is_valid.old_value.app_error", + "translation": "Некарэктнае старое значэнне, яно не павінна было быць устаноўлена, калі аперацыя не з'яўляецца атамарнай." + }, + { + "id": "model.plugin_key_value.is_valid.plugin_id.app_error", + "translation": "Няверны ідэнтыфікатар плагіна, павінен быць даўжэй {{.Min}} і мець даўжыню не больш за {{.Max}} знакаў." + }, + { + "id": "model.plugin_key_value.is_valid.key.app_error", + "translation": "Няправільны ключ, павінен быць даўжэй {{.Min}} і ўтрымоўваць не больш за {{.Max}} знакаў." + }, + { + "id": "model.plugin_command_error.error.app_error", + "translation": "Убудова для /{{.Command}} не працуе. Калі ласка, звяжыцеся з Вашым сістэмным адміністратарам" + }, + { + "id": "model.plugin_command_crash.error.app_error", + "translation": "/{{.Command}} каманда парушыла працу плагіна {{.PluginId}}. Калі ласка, звяжыцеся з Вашым сістэмным адміністратарам" + }, + { + "id": "model.plugin_command.error.app_error", + "translation": "Адбылася памылка пры спробе выканаць гэтую каманду." + }, + { + "id": "model.outgoing_hook.is_valid.words.app_error", + "translation": "Некарэктныя словы-трыгеры." + }, + { + "id": "model.outgoing_hook.is_valid.url.app_error", + "translation": "Некарэктны URL зваротнага выкліку. Адрас URL павінен быць правільным і пачацца з http:// ці https://." + }, + { + "id": "model.outgoing_hook.is_valid.update_at.app_error", + "translation": "\"Абноўлена ў\" павінна быць карэктным часам." + }, + { + "id": "model.outgoing_hook.is_valid.trigger_words.app_error", + "translation": "Некарэктныя словы-трыгеры." + }, + { + "id": "model.outgoing_hook.is_valid.team_id.app_error", + "translation": "Недапушчальны ідэнтыфікатар каманды." + }, + { + "id": "model.outgoing_hook.is_valid.create_at.app_error", + "translation": "\"Створана ў\" павінна быць карэктным часам." + }, + { + "id": "model.outgoing_hook.is_valid.content_type.app_error", + "translation": "Недапушчальнае значэнне для content_type." + }, + { + "id": "model.outgoing_hook.is_valid.channel_id.app_error", + "translation": "Няправiльны ідэнтыфікатар канала." + }, + { + "id": "model.outgoing_hook.is_valid.callback.app_error", + "translation": "Няправільныя URL-адрасы зваротнага выкліку." + }, + { + "id": "model.oauth.is_valid.update_at.app_error", + "translation": "\"Абноўлена ў\" павінна быць карэктным часам." + }, + { + "id": "model.oauth.is_valid.icon_url.app_error", + "translation": "Адрас абразка павінен быць карэктным URL-адрасам і пачынацца з http:// або https://." + }, + { + "id": "model.oauth.is_valid.homepage.app_error", + "translation": "Адрас хатняй старонкі павінен быць карэктным URL-адрасам і пачынацца з http:// або https://." + }, + { + "id": "model.oauth.is_valid.creator_id.app_error", + "translation": "Недапушчальны ідэнтыфікатар стваральніка." + }, + { + "id": "model.oauth.is_valid.create_at.app_error", + "translation": "\"Створана ў\" павінна быць карэктным часам." + }, + { + "id": "model.oauth.is_valid.client_secret.app_error", + "translation": "Некарэктны сакрэт кліента." + }, + { + "id": "model.oauth.is_valid.callback.app_error", + "translation": "Callback URL павінен быць карэктным URL-адрасам і пачынацца з http:// або https://." + }, + { + "id": "model.oauth.is_valid.app_id.app_error", + "translation": "Некарэктны ідэнтыфікатар прыкладання." + }, + { + "id": "model.link_metadata.is_valid.url.app_error", + "translation": "URL метададзеных спасылкі павінен быць зададзены." + }, + { + "id": "model.link_metadata.is_valid.type.app_error", + "translation": "Няправільны тып метададзеных спасылкі." + }, + { + "id": "model.link_metadata.is_valid.timestamp.app_error", + "translation": "Пазнака часу метададзеных спасылкі павінна быць ненулявой і акругленая да бліжэйшай гадзіны." + }, + { + "id": "model.link_metadata.is_valid.data_type.app_error", + "translation": "Дадзеныя метададзеных спасылкі не адпавядаюць зададзенаму тыпу." + }, + { + "id": "model.link_metadata.is_valid.data.app_error", + "translation": "Дадзеныя метададзеных спасылкі не могуць быць нулявымі." + }, + { + "id": "model.license_record.is_valid.id.app_error", + "translation": "Няправільнае значэнне для id пры загрузцы файла ліцэнзіі." + }, + { + "id": "model.license_record.is_valid.create_at.app_error", + "translation": "Няправільнае значэнне для create_at пры загрузцы файла ліцэнзіі." + }, + { + "id": "model.job.is_valid.type.app_error", + "translation": "Некарэктны тып задання." + }, + { + "id": "model.job.is_valid.status.app_error", + "translation": "Некарэктны статус задання." + }, + { + "id": "model.job.is_valid.id.app_error", + "translation": "Некарэктны ідэнтыфікатар задання." + }, + { + "id": "model.job.is_valid.create_at.app_error", + "translation": "\"Створана ў\" павінна быць карэктным часам." + }, + { + "id": "model.incoming_hook.update_at.app_error", + "translation": "\"Абноўлена ў\" павінна быць карэктным часам." + }, + { + "id": "model.incoming_hook.team_id.app_error", + "translation": "Няправільны ідэнтыфікатар каманды." + }, + { + "id": "model.incoming_hook.parse_data.app_error", + "translation": "Немагчыма разабраць уваходныя дадзеныя." + }, + { + "id": "model.incoming_hook.icon_url.app_error", + "translation": "Недапушчальны значок паведамлення." + }, + { + "id": "model.incoming_hook.create_at.app_error", + "translation": "\"Створана ў\" павінна быць карэктным часам." + }, + { + "id": "model.incoming_hook.channel_id.app_error", + "translation": "Няправiльны ідэнтыфікатар канала." + }, + { + "id": "model.group_syncable.syncable_id.app_error", + "translation": "Няправільны \"ідэнтыфікатар сінхранізацыі\" для сінхранізацыі групы." + }, + { + "id": "model.group_syncable.group_id.app_error", + "translation": "Няслушная ўласцівасць \"ідэнтыфікатар групы\" для групавой сінхранізацыі." + }, + { + "id": "model.group_member.user_id.app_error", + "translation": "Няслушная ўласцівасць \"ідэнтыфікатар карыстальніка\" для ўдзельніка групы." + }, + { + "id": "model.group_member.group_id.app_error", + "translation": "Няслушная ўласцівасць \"ідэнтыфікатар групы\" для ўдзельніка групы." + }, + { + "id": "model.group.update_at.app_error", + "translation": "Няправільная ўласцівасць \"абноўлена ў\" для групы." + }, + { + "id": "model.group.source.app_error", + "translation": "Няслушная ўласцівасць \"крыніца\" для групы." + }, + { + "id": "model.group.remote_id.app_error", + "translation": "Няслушная ўласцівасць \"выдалены ідэнтыфікатар\" для групы." + }, + { + "id": "model.group.name.invalid_length.app_error", + "translation": "Імя павінна мець даўжыню ад 1 да 64 сімвалаў, якія змяшчаюць літары і лічбы ў ніжнім рэгістры." + }, + { + "id": "model.group.name.invalid_chars.app_error", + "translation": "недапушчальныя сімвалы ва ўласцівасці імя для групы" + }, + { + "id": "model.group.name.app_error", + "translation": "Няслушная ўласцівасць \"імя\" для групы." + }, + { + "id": "model.group.display_name.app_error", + "translation": "Няслушная ўласцівасць \"адлюстроўванае імя\" для групы." + }, + { + "id": "model.group.description.app_error", + "translation": "Няслушная ўласцівасць \"апісанне\" для групы." + }, + { + "id": "model.group.create_at.app_error", + "translation": "Няслушная ўласцівасць \"Створана ў\" для групы." + }, + { + "id": "model.file_info.is_valid.user_id.app_error", + "translation": "Недапушчальнае значэнне для часу чакання чытання." + }, + { + "id": "model.file_info.is_valid.update_at.app_error", + "translation": "Недапушчальнае значэнне для часу чакання чытання." + }, + { + "id": "model.file_info.is_valid.post_id.app_error", + "translation": "Недапушчальнае значэнне для часу чакання чытання." + }, + { + "id": "model.file_info.is_valid.path.app_error", + "translation": "Недапушчальнае значэнне для часу чакання чытання." + }, + { + "id": "model.file_info.is_valid.id.app_error", + "translation": "Недапушчальнае значэнне для ідэнтыфікатара." + }, + { + "id": "model.file_info.is_valid.create_at.app_error", + "translation": "Недапушчальнае значэнне для create_at." + }, + { + "id": "model.file_info.get.gif.app_error", + "translation": "Не ўдалося дэкадаваць gif." + }, + { + "id": "model.emoji.user_id.app_error", + "translation": "Недапушчальны ідэнтыфікатар стваральніка." + }, + { + "id": "model.emoji.update_at.app_error", + "translation": "\"Абноўлена ў\" павінна быць карэктным часам." + }, + { + "id": "model.emoji.name.app_error", + "translation": "Імя павінна мець даўжыню ад 1 да 64 сімвалаў, якія змяшчаюць літары і лічбы ў ніжнім рэгістры." + }, + { + "id": "model.emoji.id.app_error", + "translation": "Некарэктны ідэнтыфікатар эмоджы." + }, + { + "id": "model.emoji.create_at.app_error", + "translation": "\"Створана ў\" павінна быць карэктным часам." + }, + { + "id": "model.config.is_valid.write_timeout.app_error", + "translation": "Недапушчальнае значэнне для часу чакання запісу." + }, + { + "id": "model.config.is_valid.websocket_url.app_error", + "translation": "Спасылка на шлюз WebRTC павінна быць дзейснай і пачынацца з ws:// ці wss://." + }, + { + "id": "model.config.is_valid.webserver_security.app_error", + "translation": "Недапушчальнае значэнне налад бяспекі злучэння вэб-сервера." + }, + { + "id": "model.config.is_valid.tls_overwrite_cipher.app_error", + "translation": "Перададзена недапушчальнае значэнне для шыфра перазапісу TLS - сапраўдныя значэнні гл. у дакументацыі." + }, + { + "id": "model.config.is_valid.tls_key_file_missing.app_error", + "translation": "Няслушнае значэнне для файла ключа TLS - альбо выкарыстайце LetsEncrypt, альбо ўкажыце шлях да існуючага файла ключа." + }, + { + "id": "model.config.is_valid.tls_cert_file_missing.app_error", + "translation": "Няправільнае значэнне для файла сертыфіката TLS - альбо выкарыстайце LetsEncrypt, альбо ўкажыце шлях да існуючага файла сертыфіката." + }, + { + "id": "model.config.is_valid.time_between_user_typing.app_error", + "translation": "Абнаўленні статуту набору тэксту карыстачом не могуць быць гушчару, чым раз у 1000 мілісекунд." + }, + { + "id": "model.config.is_valid.teammate_name_display.app_error", + "translation": "Няправільны паказ таварыша па камандзе. Павінна быць 'full_name, 'nickname_full_name' ці 'username'." + }, + { + "id": "model.config.is_valid.sql_query_timeout.app_error", + "translation": "Няправільны тайм-аўт запыту для налад SQL. Павінна быць станоўчым лікам." + }, + { + "id": "model.config.is_valid.sql_max_conn.app_error", + "translation": "Няправільнае значэнне максімальнай колькасці адчыненых злучэнняў у наладах SQL. Лік павінен быць дадатным." + }, + { + "id": "model.config.is_valid.sql_idle.app_error", + "translation": "Няправільнае значэнне максімальнай колькасці падлучэнняў у рэжыме чакання ў наладах SQL. Павінна быць станоўчым лікам." + }, + { + "id": "model.config.is_valid.sql_driver.app_error", + "translation": "Няправільнае імя драйвера ў наладах SQL. Павінна быць 'mysql' ці 'postgres'." + }, + { + "id": "model.config.is_valid.sql_data_src.app_error", + "translation": "Няправільная крыніца дадзеных у наладах SQL. Значэнне павінна быць устаноўлена." + }, + { + "id": "model.config.is_valid.sql_conn_max_lifetime_milliseconds.app_error", + "translation": "Няправільны максімальны час жыцця злучэння для настроек SQL. Павінна быць неадмоўным лікам." + }, + { + "id": "model.config.is_valid.sql_conn_max_idle_time_milliseconds.app_error", + "translation": "Няправільны максімальны час жыцця злучэння для настроек SQL. Павінна быць неадмоўным лікам." + }, + { + "id": "model.config.is_valid.sitename_length.app_error", + "translation": "Назва сайта павінна быць менш ці роўна {{.MaxLength}} сімвалаў." + }, + { + "id": "model.config.is_valid.site_url_email_batching.app_error", + "translation": "Немагчыма ўключыць паштовыя аб'яднанні, калі не зададзены SiteURL." + }, + { + "id": "model.config.is_valid.site_url.app_error", + "translation": "Адрас абразка павінен быць карэктным URL-адрасам і пачынацца з http:// або https://." + }, + { + "id": "model.config.is_valid.saml_username_attribute.app_error", + "translation": "Няправільнае значэнне атрыбута Username. Атрыбут павінен быць зададзены." + }, + { + "id": "model.config.is_valid.saml_spidentifier_attribute.app_error", + "translation": "Патрабуецца ідэнтыфікатар пастаўшчыка паслуг" + }, + { + "id": "model.config.is_valid.saml_public_cert.app_error", + "translation": "Адсутнічае публічны сертыфікат правайдэра. Вы забыліся яго загрузіць?" + }, + { + "id": "model.config.is_valid.saml_private_key.app_error", + "translation": "Прыватны ключ пастаўшчыка службы адсутнічае. Вы забыліся яго загрузіць?" + }, + { + "id": "model.config.is_valid.saml_idp_url.app_error", + "translation": "URL SAML SSO павінен з'яўляцца карэктным URL і пачынацца з http:// ці https://." + }, + { + "id": "model.config.is_valid.saml_idp_descriptor_url.app_error", + "translation": "URL эмітэнта пастаўшчыка уліковых дадзеных павінен з'яўляцца карэктным URL і пачынацца з http:// ці https://." + }, + { + "id": "model.config.is_valid.saml_idp_cert.app_error", + "translation": "Адсутнічае сертыфікат пастаўшчыка уліковых дадзеных. Вы забыліся яго загрузіць?" + }, + { + "id": "model.config.is_valid.saml_guest_attribute.app_error", + "translation": "Няправільны атрыбут госця. Павінен быць у форме 'поле=значэнне'." + }, + { + "id": "model.config.is_valid.saml_email_attribute.app_error", + "translation": "Няправільнае значэнне атрыбуту Электронная пошта. Павінна быць зададзена." + }, + { + "id": "model.config.is_valid.saml_assertion_consumer_service_url.app_error", + "translation": "URL уваходу пастаўшчыка паслуг павінен з'яўляцца карэктным URL-адрасам і пачынацца з http:// або https://." + }, + { + "id": "model.config.is_valid.saml_admin_attribute.app_error", + "translation": "Няправільны атрыбут адміністратара. Павінен быць у форме 'поле=значэнне'." + }, + { + "id": "model.config.is_valid.restrict_direct_message.app_error", + "translation": "Няправільнае абмежаванне асабістых паведамленняў. Павінна быць 'any' ці 'team'." + }, + { + "id": "model.config.is_valid.read_timeout.app_error", + "translation": "Недапушчальнае значэнне для часу чакання чытання." + }, + { + "id": "model.config.is_valid.rate_sec.app_error", + "translation": "Няправільнае значэнне \"У секунду\" ў наладах абмежавання хуткасці. Павінна быць станоўчым лікам." + }, + { + "id": "model.config.is_valid.rate_mem.app_error", + "translation": "Няправільны памер сховішчы памяці ў наладах абмежавання хуткасці. Павінен быць станоўчым лікам." + }, + { + "id": "model.config.is_valid.password_length.app_error", + "translation": "Мінімальная даўжыня пароля павінна быць цэлым лікам вялікім ці роўным {{.MinLength}}, а таксама меншым або роўным {{.MaxLength}}." + }, + { + "id": "model.config.is_valid.message_export.global_relay.smtp_username.app_error", + "translation": "GlobalRelaySettings.SmtpUsername заданні экспарту паведамленняў павінна быць зададзена." + }, + { + "id": "model.config.is_valid.message_export.global_relay.smtp_password.app_error", + "translation": "GlobalRelaySettings.SmtpPassword заданні экспарту паведамленняў павінен быць зададзены." + }, + { + "id": "model.config.is_valid.message_export.global_relay.email_address.app_error", + "translation": "GlobalRelaySettings.EmailAddress заданні экспарту паведамленняў павінен быць сапраўдным адрасам электроннай пошты." + }, + { + "id": "model.config.is_valid.message_export.global_relay.customer_type.app_error", + "translation": "GlobalRelaySettings.CustomerType экспарту паведамленняў павінен быць усталяваны ў 'A9' ці 'A10'." + }, + { + "id": "model.config.is_valid.message_export.global_relay.config_missing.app_error", + "translation": "ExportFormat заданні экспарту паведамленняў устаноўлены на 'globalrelay', але GlobalRelaySettings адсутнічаюць." + }, + { + "id": "model.config.is_valid.message_export.export_type.app_error", + "translation": "ExportFormat заданні экспарту паведамленняў павінен быць 'actiance', 'csv' ці 'globalrelay'." + }, + { + "id": "model.config.is_valid.message_export.export_from.app_error", + "translation": "ExportFromTimestamp заданні экспарту паведамленняў павінна быць адзнакай часу (выяўляецца ў секундах з пачатку эпохі Unix). Толькі паведамленні, адпраўленыя пасля гэтай адзнакі часу, будуць экспартаваны." + }, + { + "id": "model.config.is_valid.message_export.enable.app_error", + "translation": "Параметр EnableExport задання экспарту паведамленняў павінна быць роўна true ці false." + }, + { + "id": "model.config.is_valid.message_export.daily_runtime.app_error", + "translation": "Заданне экспарту паведамлення DailyRuntime павінна быць 24-гадзіннай адзнакай часу ў форме ЧЧ:ММ." + }, + { + "id": "model.config.is_valid.message_export.batch_size.app_error", + "translation": "BatchSize заданні экспарту паведамленняў павінна быць станоўчым цэлым лікам." + }, + { + "id": "model.config.is_valid.max_users.app_error", + "translation": "Няправільнае максімальную колькасць карыстальнікаў на каманду ў наладах каманд. Лік павінен быць дадатным." + }, + { + "id": "model.config.is_valid.max_notify_per_channel.app_error", + "translation": "Няправільнае максімальную колькасць карыстальнікаў на каманду. Лік павінен быць дадатным." + }, + { + "id": "model.config.is_valid.max_file_size.app_error", + "translation": "Няправільны максімальны памер файла для настроек файла. Павінна быць цэлым лікам большым за нуль." + }, + { + "id": "model.config.is_valid.max_channels.app_error", + "translation": "Няправільнае максімальную колькасць карыстальнікаў на каманду ў наладах каманд. Лік павінен быць дадатным." + }, + { + "id": "model.config.is_valid.max_burst.app_error", + "translation": "Максімальнае перавышэнне паласы прапускання павінна быць больш за нуль." + }, + { + "id": "model.config.is_valid.login_attempts.app_error", + "translation": "Няслушная максімальная колькасць спроб уваходу ў настройках службы. Павінна быць станоўчым лікам." + }, + { + "id": "model.config.is_valid.localization.available_locales.app_error", + "translation": "Даступныя мовы павінны змяшчаць мову кліента па змаўчанні." + }, + { + "id": "model.config.is_valid.listen_address.app_error", + "translation": "Няправільны адрас праслухоўвання ў настройках службы. Павінен быць зададзены." + }, + { + "id": "model.config.is_valid.ldap_username", + "translation": "Патрабуецца поле AD/LDAP \"Username Attribute\"." + }, + { + "id": "model.config.is_valid.ldap_sync_interval.app_error", + "translation": "Няправільны інтэрвал сінхранізацыі. Павінен складаць не менш за адну хвіліну." + }, + { + "id": "model.config.is_valid.ldap_server", + "translation": "Патрабуецца поле AD/LDAP \"AD/LDAP Server\"." + }, + { + "id": "model.config.is_valid.ldap_security.app_error", + "translation": "Няправільны тып бяспекі злучэння ў настройках AD/LDAP. Павінна быць '', 'TLS', ці 'STARTTLS'." + }, + { + "id": "model.config.is_valid.ldap_max_page_size.app_error", + "translation": "Няправільнае значэнне максімальнага памеру старонкі." + }, + { + "id": "model.config.is_valid.ldap_login_id", + "translation": "Патрабуецца поле AD/LDAP \"ID Attribute\"." + }, + { + "id": "model.config.is_valid.ldap_id", + "translation": "Патрабуецца поле AD/LDAP \"ID Attribute\"." + }, + { + "id": "model.config.is_valid.ldap_email", + "translation": "Патрабуецца поле AD/LDAP \"Email Attribute\"." + }, + { + "id": "model.config.is_valid.ldap_basedn", + "translation": "Патрабуецца поле AD/LDAP \"BaseDN\"." + }, + { + "id": "model.config.is_valid.import.retention_days_too_low.app_error", + "translation": "Некарэктнае значэнне для RetentionDays. Занадта маленькае значэньне." + }, + { + "id": "model.config.is_valid.import.directory.app_error", + "translation": "Некарэктнае значэнне для Directory." + }, + { + "id": "model.config.is_valid.image_proxy_type.app_error", + "translation": "Няправільны тып выявы проксі. Павінна быць 'local' ці 'atmos/camo'." + }, + { + "id": "model.config.is_valid.group_unread_channels.app_error", + "translation": "Няправільная група непрачытаных каналаў для настроек сэрвісу. Павінна быць 'disabled', 'default_on', ці 'default_off'." + }, + { + "id": "model.config.is_valid.file_salt.app_error", + "translation": "Няправільная соль публічнай спасылкі ў наладах файлаў. Павінна складацца з 32 і больш сімвалаў." + }, + { + "id": "model.config.is_valid.file_driver.app_error", + "translation": "Няправільнае імя драйвера ў наладах файлаў. Павінна быць 'local' ці 'amazons3'." + }, + { + "id": "model.config.is_valid.export.retention_days_too_low.app_error", + "translation": "Несапраўднае значэнне для RetentionDays. Значэнне павінна быць больш за 0" + }, + { + "id": "model.config.is_valid.export.directory.app_error", + "translation": "Значэнне для Directory не павінна быць пустым." + }, + { + "id": "model.config.is_valid.encrypt_sql.app_error", + "translation": "Няправільны ключ шыфравання ў наладах SQL. Павінен быць 32 знакі або больш." + }, + { + "id": "model.config.is_valid.email_security.app_error", + "translation": "Няправільны тып бяспекі злучэння ў настройках пошты. Павінна быць '', 'TLS', ці 'STARTTLS'." + }, + { + "id": "model.config.is_valid.email_notification_contents_type.app_error", + "translation": "Недапушчальны тып змесціва апавяшчэння па электроннай пошце для настроек электроннай пошты. Павінна быць 'full' ці 'generic'." + }, + { + "id": "model.config.is_valid.email_batching_interval.app_error", + "translation": "Няправільны інтэрвал адпраўкі паштовых аб'яднанняў у настройках пошты. Павінен складаць 30 секунд і больш." + }, + { + "id": "model.config.is_valid.email_batching_buffer_size.app_error", + "translation": "Няправільны памер буфера паштовага аб'яднання ў наладах пошты. Лік павінен быць неадмоўным." + }, + { + "id": "model.config.is_valid.elastic_search.request_timeout_seconds.app_error", + "translation": "Час чакання запыту Elasticsearch павінна складаць не менш за 1 секунду." + }, + { + "id": "model.config.is_valid.elastic_search.posts_aggregator_job_start_time.app_error", + "translation": "Параметр Elasticsearch PostsAggregatorJobStartTime павінен быць часам у фармаце \"чч:мм\"." + }, + { + "id": "model.config.is_valid.elastic_search.live_indexing_batch_size.app_error", + "translation": "Памер пакета жывога індэксавання Elasticsearch павінен быць не менш за 1." + }, + { + "id": "model.config.is_valid.elastic_search.enable_searching.app_error", + "translation": "Для параметра Elasticsearch EnableIndexing павінна быць устаноўлена значэнне true, калі для параметра Elasticsearch SearchEnabled устаноўлена значэнне true" + }, + { + "id": "model.config.is_valid.elastic_search.enable_autocomplete.app_error", + "translation": "Параметр Elasticsearch IndexingEnabled павінен мець значэнне true, калі для Elasticsearch AutocompleteEnabled устаноўлена значэнне true" + }, + { + "id": "model.config.is_valid.elastic_search.connection_url.app_error", + "translation": "Elastic Search ConnectionUrl setting must be provided when Elastic Search indexing is enabled." + }, + { + "id": "model.config.is_valid.elastic_search.aggregate_posts_after_days.app_error", + "translation": "Параметр Elasticsearch AggregatePostsAfterDays павінен быць лікам большым ці роўным 1." + }, + { + "id": "model.config.is_valid.display.custom_url_schemes.app_error", + "translation": "The custom URL scheme {{.Scheme}} is invalid. Custom URL schemes must start with letter and contain only letters, numbers and hyphen (-)." + }, + { + "id": "model.config.is_valid.directory.app_error", + "translation": "Няправільны каталог лакальнага сховішча. Павінен быць непусты радок." + }, + { + "id": "model.config.is_valid.data_retention.message_retention_days_too_low.app_error", + "translation": "Тэрмін захоўвання паведамлення павінен складаць адзін дзень ці больш." + }, + { + "id": "model.config.is_valid.data_retention.file_retention_days_too_low.app_error", + "translation": "Тэрмін захоўвання файла павінен быць адзін дзень ці даўжэй." + }, + { + "id": "model.config.is_valid.data_retention.deletion_job_start_time.app_error", + "translation": "Час пачатку захавання даных павінен быць 24-гадзіннай адзнакай у фармаце ЧЧ:ММ." + }, + { + "id": "model.config.is_valid.collapsed_threads.autofollow.app_error", + "translation": "Для ўключэння CollapsedThreads павінна быць усталявана значэнне ThreadAutoFollow" + }, + { + "id": "model.config.is_valid.collapsed_threads.app_error", + "translation": "Налада CollapsedThreads павінна быць альбо disabled, альбо default_on, альбо default_off" + }, + { + "id": "model.config.is_valid.cluster_email_batching.app_error", + "translation": "Немагчыма ўключыць пакетную апрацоўку пошты пры ўключанай кластарызацыі." + }, + { + "id": "model.config.is_valid.bleve_search.filename.app_error", + "translation": "Параметр Bleve IndexingDir павінен быць усталяваны, калі для параметра Bleve EnableIndexing усталявана значэнне true" + }, + { + "id": "model.config.is_valid.bleve_search.enable_searching.app_error", + "translation": "Параметр Bleve EnableIndexing павінен мець значэнне true, калі для Bleve EnableSearching устаноўлена значэнне true" + }, + { + "id": "model.config.is_valid.bleve_search.enable_autocomplete.app_error", + "translation": "Параметр Bleve EnableIndexing павінен мець значэнне true, калі для Bleve EnableAutocomplete устаноўлена значэнне true" + }, + { + "id": "model.config.is_valid.atmos_camo_image_proxy_url.app_error", + "translation": "Недапушчальны RemoteImageProxyURL для atmos/camo. Павінен быць устаноўлены на ваш агульны ключ." + }, + { + "id": "model.config.is_valid.atmos_camo_image_proxy_options.app_error", + "translation": "Недапушчальныя RemoteImageProxyOptions для atmos/camo. Павінен быць устаноўлены на ваш агульны ключ." + }, + { + "id": "model.config.is_valid.allow_cookies_for_subdomains.app_error", + "translation": "Для дазволу файлаў cookie для паддаменаў неабходна ўсталяваць SiteURL." + }, + { + "id": "model.compliance.is_valid.start_end_at.app_error", + "translation": "\"Да\" павінна быць больш, чым \"З\"." + }, + { + "id": "model.compliance.is_valid.start_at.app_error", + "translation": "\"З\" павінна быць карэктным часам." + }, + { + "id": "model.compliance.is_valid.end_at.app_error", + "translation": "\"Да\" павінна быць карэктным часам." + }, + { + "id": "model.compliance.is_valid.create_at.app_error", + "translation": "\"Створана ў\" павінна быць карэктным часам." + }, + { + "id": "model.command_hook.id.app_error", + "translation": "Недапушчальны ідэнтыфікатар Хука каманды." + }, + { + "id": "model.command_hook.create_at.app_error", + "translation": "\"Створана ў\" павінна быць карэктным часам." + }, + { + "id": "model.command.is_valid.url_http.app_error", + "translation": "Недапушчальны URL. Адрас URL павінен быць правільны і пачынацца з http:// ці https://." + }, + { + "id": "model.command.is_valid.update_at.app_error", + "translation": "\"Абноўлена ў\" павінна быць карэктным часам." + }, + { + "id": "model.command.is_valid.create_at.app_error", + "translation": "\"Створана ў\" павінна быць карэктным часам." + }, + { + "id": "model.cluster.is_valid.type.app_error", + "translation": "Тып павінен быць зададзены." + }, + { + "id": "model.cluster.is_valid.name.app_error", + "translation": "ClusterName павінен быць зададзены." + }, + { + "id": "model.cluster.is_valid.last_ping_at.app_error", + "translation": "LastPingAt павінен быць зададзены." + }, + { + "id": "model.cluster.is_valid.hostname.app_error", + "translation": "Імя хаста павінна быць зададзена." + }, + { + "id": "model.cluster.is_valid.create_at.app_error", + "translation": "CreateAt павінен быць зададзены." + }, + { + "id": "model.channel_member.is_valid.unread_level.app_error", + "translation": "Немагчыма пазначыць як непрачытанае: няслушны ўзровень дазволу." + }, + { + "id": "model.channel_member.is_valid.push_level.app_error", + "translation": "Няправільны ўзровень push-апавяшчэнняў." + }, + { + "id": "model.channel_member.is_valid.ignore_channel_mentions_value.app_error", + "translation": "Няправільны статус ігнаравання згадак у канале." + }, + { + "id": "model.channel_member.is_valid.email_value.app_error", + "translation": "Няправільнае значэнне апавяшчэння па электроннай пошце." + }, + { + "id": "model.channel.is_valid.update_at.app_error", + "translation": "\"Абноўлена ў\" павінна быць карэктным часам." + }, + { + "id": "model.channel.is_valid.name.app_error", + "translation": "Некарэктнае імя канала. Забаронена выкарыстоўваць ідэнтыфікатары карыстальнікаў у якасці імя канала (акрамя каналаў асабістых паведамленняў)." + }, + { + "id": "model.channel.is_valid.create_at.app_error", + "translation": "\"Створана ў\" павінна быць карэктным часам." + }, + { + "id": "model.authorize.is_valid.expires.app_error", + "translation": "Тэрмін дзеяння павінен быць устаноўлены." + }, + { + "id": "model.authorize.is_valid.create_at.app_error", + "translation": "\"Створана ў\" павінна быць карэктным часам." + }, + { + "id": "migrations.worker.run_migration.unknown_key", + "translation": "Немагчыма запусціць заданне міграцыі з-за невядомага ключа міграцыі." + }, + { + "id": "migrations.worker.run_advanced_permissions_phase_2_migration.invalid_progress", + "translation": "Міграцыя не ўдалася з-за няправільных звестак аб прагрэсе." + }, + { + "id": "migrations.system.save.app_error", + "translation": "Узнікла памылка пры захаванні ўласцівасці сістэмы." + }, + { + "id": "mfa.mfa_disabled.app_error", + "translation": "Шматфактарная аўтэнтыфікацыя была адключаная на гэтым серверы." + }, + { + "id": "mfa.generate_qr_code.create_code.app_error", + "translation": "Памылка генерацыі QR-кода." + }, + { + "id": "mfa.deactivate.app_error", + "translation": "Немагчыма абнавіць актыўны статус MFA для карыстальніка." + }, + { + "id": "mfa.activate.app_error", + "translation": "Немагчыма абнавіць актыўны статус MFA для карыстальніка." + }, + { + "id": "manaultesting.test_autolink.unable.app_error", + "translation": "Немагчыма атрымаць каналы." + }, + { + "id": "manaultesting.manual_test.parse.app_error", + "translation": "Немагчыма разабраць URL." + }, + { + "id": "jobs.set_job_error.update.error", + "translation": "Не ўдалося ўстанавіць статус задання на памылку" + }, + { + "id": "jobs.request_cancellation.status.error", + "translation": "Не ўдалося запытаць адмену для задання, якое не знаходзіцца ў стане адмены." + }, + { + "id": "interactive_message.generate_trigger_id.signing_failed", + "translation": "Не атрымалася падпісаць згенераваны ID трыгера для інтэрактыўнага дыялогу." + }, + { + "id": "interactive_message.decode_trigger_id.verify_signature_failed", + "translation": "Памылка пацверджання подпісу ID трыгера для інтэрактыўнага дыялогу." + }, + { + "id": "interactive_message.decode_trigger_id.signature_decode_failed", + "translation": "Не атрымалася дэкадаваць base64 подпіс ID трыгера для інтэрактыўнага дыялогу." + }, + { + "id": "interactive_message.decode_trigger_id.missing_data", + "translation": "Трыгер ID не ўтрымоўвае патрабаваных дадзеных для інтэрактыўнага дыялогу." + }, + { + "id": "interactive_message.decode_trigger_id.expired", + "translation": "ID трыгера для інтэрактыўнага дыялогу скончыўся. ID трыгераў сапраўдныя на працягу максімум {{.Seconds}} секунд." + }, + { + "id": "interactive_message.decode_trigger_id.base64_decode_failed_signature", + "translation": "Не атрымалася дэкадаваць base64 подпіс ID трыгера для інтэрактыўнага дыялогу." + }, + { + "id": "interactive_message.decode_trigger_id.base64_decode_failed", + "translation": "Не атрымалася дэкадаваць base64 згенераваны ID трыгера для інтэрактыўнага дыялогу." + }, + { + "id": "import_process.worker.do_job.open_file", + "translation": "Немагчыма апрацаваць імпарт: не ўдалося адкрыць файл." + }, + { + "id": "import_process.worker.do_job.missing_jsonl", + "translation": "Не ўдалося апрацаваць імпарт: JSONL файл адсутнічае." + }, + { + "id": "import_process.worker.do_job.missing_file", + "translation": "Не ўдалося апрацаваць імпарт: адсутнічае параметр import_file." + }, + { + "id": "import_process.worker.do_job.file_exists", + "translation": "Не ўдалося апрацаваць імпарт: файл не існуе." + }, + { + "id": "groups.unsupported_syncable_type", + "translation": "Непадтрымоўваны сінхранізаваны тып '{{.Value}}'." + }, + { + "id": "group_not_associated_to_synced_team", + "translation": "Група не можа быць звязана з каналам, пакуль яна не будзе спачатку звязана з бацькоўскай сінхранізаванай па групе камандай." + }, + { + "id": "ent.user.complete_switch_with_oauth.blank_email.app_error", + "translation": "Немагчыма завяршыць уваход у SAML з пустым адрасам электроннай пошты." + }, + { + "id": "ent.saml.service_disable.app_error", + "translation": "SAML 2.0 не наладжаны ці не падтрымліваецца на гэтым серверы." + }, + { + "id": "ent.saml.save_user.username_exists.saml_app_error", + "translation": "Уліковы запіс з такім імем карыстальніка ўжо існуе. Калі ласка, звяжыцеся з Адміністратарам." + }, + { + "id": "ent.saml.save_user.email_exists.saml_app_error", + "translation": "Гэты ўліковы запіс не выкарыстоўвае аўтэнтыфікацыю SAML. Калі ласка, увайдзіце, выкарыстоўваючы электронную пошту і пароль." + }, + { + "id": "ent.saml.metadata.app_error", + "translation": "Адбылася памылка пры пабудове метададзеных пастаўшчыка паслуг." + }, + { + "id": "ent.saml.license_disable.app_error", + "translation": "Ваша ліцэнзія не падтрымлівае SAML аўтэнтыфікацыю." + }, + { + "id": "ent.saml.do_login.validate.app_error", + "translation": "Узнікла памылка падчас праверкі адказу ад пастаўшчыка уліковых запісаў. Калі ласка, звяжыцеся з сістэмным адміністратарам." + }, + { + "id": "ent.saml.do_login.parse.app_error", + "translation": "Узнікла памылка падчас разбору адказу ад пастаўшчыка уліковых запісаў. Калі ласка, звяжыцеся з сістэмным адміністратарам." + }, + { + "id": "ent.saml.do_login.invalid_time.app_error", + "translation": "Мы атрымалі няправільны час у адказе пастаўшчыка ідэнтыфікацыйнай інфармацыі. Калі ласка, звярніцеся да сістэмнага адміністратара." + }, + { + "id": "ent.saml.do_login.invalid_signature.app_error", + "translation": "Узнікла памылка падчас праверкі адказу ад правайдэра ўліковых запісаў. Калі ласка, звяжыцеся з сістэмным адміністратарам." + }, + { + "id": "ent.saml.do_login.empty_response.app_error", + "translation": "Атрыманы пусты адказ ад пастаўшчыка уліковых запісаў." + }, + { + "id": "ent.saml.do_login.decrypt.app_error", + "translation": "Спроба ўваходу з выкарыстаннем SAML не ўдалася з-за ўзнікнення памылкі пры расшыфроўцы адказу ад пастаўшчыка уліковых запісаў. Калі ласка, звяжыцеся з сістэмным адміністратарам." + }, + { + "id": "ent.saml.configure.not_encrypted_response.app_error", + "translation": "Спроба ўваходу з выкарыстаннем SAML не атрымалася, бо адказ пастаўшчыка уліковых запісаў не зашыфраваны. Калі ласка, звяжыцеся з сістэмным адміністратарам." + }, + { + "id": "ent.saml.configure.load_private_key.app_error", + "translation": "Спроба ўваходу з выкарыстаннем SAML не ўдалася з-за таго, што не быў знойдзены прыватны ключ пастаўшчыка службы. Калі ласка, звяжыцеся з сістэмным адміністратарам." + }, + { + "id": "ent.saml.configure.load_idp_cert.app_error", + "translation": "Публічны сертыфікат правайдэра ідэнтыфікацыі не знойдзены. Калі ласка, звярніцеся да сістэмнага адміністратара." + }, + { + "id": "ent.saml.configure.encryption_not_enabled.app_error", + "translation": "Спроба ўваходу з выкарыстаннем SAML не атрымалася з прычыны адключанага шыфравання. Калі ласка, звяжыцеся з сістэмным адміністратарам." + }, + { + "id": "ent.saml.build_request.encoding.app_error", + "translation": "Падчас кадавання запыту да правайдэра ідэнтыфікацыі адбылася памылка. Калі ласка, звярніцеся да сістэмнага адміністратара." + }, + { + "id": "ent.saml.build_request.app_error", + "translation": "Падчас ініцыялізацыі запыту да правайдэра ідэнтыфікацыі адбылася памылка. Калі ласка, звярніцеся да сістэмнага адміністратара." + }, + { + "id": "ent.saml.attribute.app_error", + "translation": "Спроба ўваходу з выкарыстаннем SAML не ўдалася з-за некарэктнага атрыбуту. Калі ласка, звяжыцеся з сістэмным адміністратарам." + }, + { + "id": "ent.migration.migratetosaml.username_already_used_by_other_user", + "translation": "Імя карыстальніка ўжо выкарыстоўваецца іншым карыстальнікам Mattermost." + }, + { + "id": "ent.migration.migratetosaml.user_not_found_in_users_mapping_file", + "translation": "Карыстальнік не знойдзены ў файле карыстальнікаў." + }, + { + "id": "ent.migration.migratetosaml.email_already_used_by_other_user", + "translation": "Электронная пошта ўжо выкарыстоўваецца іншым карыстальнікам SAML." + }, + { + "id": "ent.migration.migratetoldap.user_not_found", + "translation": "Не атрымалася знайсці карыстальніка на серверы AD/LDAP: " + }, + { + "id": "ent.migration.migratetoldap.duplicate_field", + "translation": "Немагчыма перанесці карыстальнікаў AD/LDAP з пазначаным полем. Выяўлена дубліраванне запісаў. Калі ласка, выдаліце ўсе дублікаты і паспрабуйце яшчэ раз." + }, + { + "id": "ent.message_export.run_export.app_error", + "translation": "Не ўдалося выбраць дадзеныя экспарту паведамленняў." + }, + { + "id": "ent.message_export.global_relay_export.get_attachment_error", + "translation": "Не атрымалася атрымаць інфармацыю аб файле для паведамлення." + }, + { + "id": "ent.message_export.global_relay_export.deliver.unable_to_open_zip_file_data.app_error", + "translation": "Немагчыма адкрыць часовы файл экспарту." + }, + { + "id": "ent.message_export.global_relay_export.deliver.unable_to_open_email_file.app_error", + "translation": "Немагчыма атрымаць ліст з часавага файла." + }, + { + "id": "ent.message_export.global_relay_export.deliver.unable_to_get_file_info.app_error", + "translation": "Немагчыма атрымаць інфармацыю аб часовым файле экспарту." + }, + { + "id": "ent.message_export.global_relay_export.deliver.unable_to_connect_smtp_server.app_error", + "translation": "Не ўдалося падключыцца да сервера smtp" + }, + { + "id": "ent.message_export.global_relay_export.deliver.to_address.app_error", + "translation": "Немагчыма ўсталяваць адрас \"Каму\" электроннай пошты." + }, + { + "id": "ent.message_export.global_relay_export.deliver.parse_mail.app_error", + "translation": "Немагчыма прачытаць інфармацыю электроннай пошты." + }, + { + "id": "ent.message_export.global_relay_export.deliver.msg_data.app_error", + "translation": "Немагчыма напісаць паведамленне электроннай пошты." + }, + { + "id": "ent.message_export.global_relay_export.deliver.msg.app_error", + "translation": "Немагчыма ўсталяваць паведамленне электроннай пошты." + }, + { + "id": "ent.message_export.global_relay_export.deliver.from_address.app_error", + "translation": "Немагчыма ўсталяваць адрас \"Ад каго\" электроннай пошты." + }, + { + "id": "ent.message_export.global_relay_export.deliver.close.app_error", + "translation": "Немагчыма даставіць ліст у Global Relay." + }, + { + "id": "ent.message_export.global_relay.generate_email.app_error", + "translation": "Немагчыма згенераваць дадзеныя файла eml." + }, + { + "id": "ent.message_export.global_relay.create_file_in_zip.app_error", + "translation": "Немагчыма стварыць файл eml." + }, + { + "id": "ent.message_export.global_relay.close_zip_file.app_error", + "translation": "Немагчыма зачыніць zip-файл." + }, + { + "id": "ent.message_export.global_relay.attach_file.app_error", + "translation": "Немагчыма дадаць укладанне ў экспарт Global Relay." + }, + { + "id": "ent.message_export.csv_export.get_attachment_error", + "translation": "Не атрымалася атрымаць інфармацыю аб файле для паведамлення." + }, + { + "id": "ent.message_export.actiance_export.get_attachment_error", + "translation": "Не атрымалася атрымаць інфармацыю аб файле для паведамлення." + }, + { + "id": "ent.ldap_groups.reachable_groups_error", + "translation": "памылка пры атрыманні груп для карыстальніка" + }, + { + "id": "ent.ldap_groups.no_rows", + "translation": "не знойдзена груп з супадальным uid" + }, + { + "id": "ent.ldap_groups.members_of_group_error", + "translation": "памылка пры атрыманні ўдзельнікаў групы" + }, + { + "id": "ent.ldap_groups.groups_search_error", + "translation": "памылка пры атрыманні груп ldap" + }, + { + "id": "ent.ldap_groups.group_search_error", + "translation": "памылка пры атрыманні групы ldap" + }, + { + "id": "ent.ldap.validate_guest_filter.app_error", + "translation": "Няверны фільтр госця AD/LDAP." + }, + { + "id": "ent.ldap.validate_admin_filter.app_error", + "translation": "Няправільны фільтр адміна AD/LDAP." + }, + { + "id": "ent.ldap.syncronize.search_failure_size_exceeded.app_error", + "translation": "Перавышаны мяжа памеру. Паспрабуйце праверыць [максімальны памер старонкі](https://docs.mattermost.com/deployment/sso-ldap.html#i-see-the-log-error-ldap-result-code-4-size-limit-exceeded) ." + }, + { + "id": "ent.ldap.syncronize.search_failure.app_error", + "translation": "Не ўдалося знайсці карыстальнікаў у AD/LDAP. Праверце, ці можа сервер Mattermost падключыцца да вашага сервера AD/LDAP, і паспрабуйце зноў." + }, + { + "id": "app.webhooks.update_incoming.app_error", + "translation": "Не атрымалася абнавіць уваходны вебхук." + }, + { + "id": "app.webhooks.save_outgoing.override.app_error", + "translation": "Вы не можаце перазапісаць наяўны выходны вебхук." + }, + { + "id": "app.webhooks.save_outgoing.app_error", + "translation": "Не атрымалася захаваць выходны вебхук." + }, + { + "id": "app.webhooks.save_incoming.existing.app_error", + "translation": "Вы не можаце перазапісаць існуючы які ўваходзіць вебхук." + }, + { + "id": "app.webhooks.save_incoming.app_error", + "translation": "Немагчыма захаваць уваходны вебхук." + }, + { + "id": "app.webhooks.permanent_delete_outgoing_by_user.app_error", + "translation": "Немагчыма выдаліць вебхук." + }, + { + "id": "app.webhooks.permanent_delete_outgoing_by_channel.app_error", + "translation": "Немагчыма выдаліць вебхук." + }, + { + "id": "app.webhooks.permanent_delete_incoming_by_user.app_error", + "translation": "Немагчыма выдаліць вебхук." + }, + { + "id": "app.webhooks.permanent_delete_incoming_by_channel.app_error", + "translation": "Немагчыма выдаліць вебхук." + }, + { + "id": "app.webhooks.get_outgoing_by_team.app_error", + "translation": "Не ўдалося атрымаць вэбхукі." + }, + { + "id": "app.webhooks.get_outgoing_by_channel.app_error", + "translation": "Не ўдалося атрымаць вэбхукі." + }, + { + "id": "app.webhooks.get_outgoing.app_error", + "translation": "Не ўдалося атрымаць вэбхук." + }, + { + "id": "app.webhooks.get_incoming_by_user.app_error", + "translation": "Не ўдалося атрымаць вэбхук." + }, + { + "id": "app.webhooks.get_incoming_by_channel.app_error", + "translation": "Не ўдалося атрымаць вэбхукі." + }, + { + "id": "app.webhooks.get_incoming.app_error", + "translation": "Не ўдалося атрымаць вэбхук." + }, + { + "id": "app.webhooks.delete_outgoing.app_error", + "translation": "Немагчыма выдаліць вебхук." + }, + { + "id": "app.webhooks.delete_incoming.app_error", + "translation": "Немагчыма выдаліць вебхук." + }, + { + "id": "app.webhooks.analytics_outgoing_count.app_error", + "translation": "Немагчыма злічыць выходныя вэб-хукі." + }, + { + "id": "app.webhooks.analytics_incoming_count.app_error", + "translation": "Немагчыма злічыць якія ўваходзяць вэб-хукі." + }, + { + "id": "app.valid_password_generic.app_error", + "translation": "Пароль несапраўдны" + }, + { + "id": "app.user_terms_of_service.save.app_error", + "translation": "Не ўдалося захаваць умовы выкарыстання." + }, + { + "id": "app.user_terms_of_service.get_by_user.no_rows.app_error", + "translation": "Умовы выкарыстання не знойдзены." + }, + { + "id": "app.user_terms_of_service.get_by_user.app_error", + "translation": "Не ўдалося атрымаць Умовы выкарыстання." + }, + { + "id": "app.user_terms_of_service.delete.app_error", + "translation": "Не атрымалася выдаліць умовы выкарыстання." + }, + { + "id": "app.user_access_token.update_token_enable.app_error", + "translation": "Немагчыма ўключыць токен доступу." + }, + { + "id": "app.user_access_token.update_token_disable.app_error", + "translation": "Немагчыма адключыць токен доступу." + }, + { + "id": "app.user_access_token.search.app_error", + "translation": "Пры пошуку токенаў доступу карыстальніка адбылася памылка." + }, + { + "id": "app.user_access_token.save.app_error", + "translation": "Не ўдалося захаваць персанальны токен доступу." + }, + { + "id": "app.user_access_token.invalid_or_missing", + "translation": "Няверны ці адсутны токен." + }, + { + "id": "app.user_access_token.get_by_user.app_error", + "translation": "Не ўдалося атрымаць персанальныя токены доступу па карыстальніку." + }, + { + "id": "app.user_access_token.get_all.app_error", + "translation": "Не ўдалося атрымаць усе персанальныя токены доступу." + }, + { + "id": "app.user_access_token.disabled", + "translation": "Такены асабістага доступу на гэтым серверы адключаныя. Калі ласка, звяжыцеся з вашым сістэмным адміністратарам для дэталяў." + }, + { + "id": "app.user_access_token.delete.app_error", + "translation": "Немагчыма выдаліць персанальны токен доступу." + }, + { + "id": "app.user.verify_email.app_error", + "translation": "Не ўдалося абнавіць поле праверкі email-адрасу." + }, + { + "id": "app.user.update_update.app_error", + "translation": "Не ўдалося абнавіць дату апошняй змены дадзеных карыстальніка." + }, + { + "id": "app.user.update_threads_read_for_user.app_error", + "translation": "Не атрымалася пазначыць усе карыстацкія абмеркаванні як прачытаныя" + }, + { + "id": "app.user.update_thread_read_for_user.app_error", + "translation": "Не ўдалося абнавіць статус чытання для абмеркавання" + }, + { + "id": "app.user.update_thread_follow_for_user.app_error", + "translation": "Немагчыма абнавіць стан адсочвання для абмеркавання" + }, + { + "id": "app.user.update_failed_pwd_attempts.app_error", + "translation": "Немагчыма абнавіць failed_attempts." + }, + { + "id": "app.user.update_auth_data.email_exists.app_error", + "translation": "Немагчыма пераключыць уліковы запіс на {{.Service}}. Уліковы запіс, які выкарыстоўвае email-адрас {{.Email}} ужо існуе." + }, + { + "id": "app.user.update_auth_data.app_error", + "translation": "Не ўдалося абнавіць дадзеныя для аўтэнтыфікацыі." + }, + { + "id": "app.user.update_active_for_multiple_users.updating.app_error", + "translation": "Не ўдалося дэактываваць гасцей." + }, + { + "id": "app.user.update.finding.app_error", + "translation": "Узнікла памылка пошуку ўліковага запісу." + }, + { + "id": "app.user.update.find.app_error", + "translation": "Не ўдалося знайсці дзейны ўліковы запіс для абнаўлення." + }, + { + "id": "app.user.store_is_empty.app_error", + "translation": "Не ўдалося праверыць, ці пуста сховішча карыстальнікаў." + }, + { + "id": "app.user.send_emails.app_error", + "translation": "Ніводнае email-паведамленне не было паспяхова адпраўлена" + }, + { + "id": "app.user.send_auto_response.app_error", + "translation": "Не ўдалося адправіць аўтаматычны адказ ад карыстальніка." + }, + { + "id": "app.user.search.app_error", + "translation": "Немагчыма знайсці карыстальніка, які адпавядае крытэрам пошуку." + }, + { + "id": "app.user.save.username_exists.app_error", + "translation": "Уліковы запіс з такім імем карыстальніка ўжо існуе." + }, + { + "id": "app.user.save.existing.app_error", + "translation": "Неабходна выклікаць абнаўленне для існуючага карыстальніка." + }, + { + "id": "app.user.save.email_exists.app_error", + "translation": "Уліковы запіс з такім email-адрасам ужо існуе." + }, + { + "id": "app.user.save.app_error", + "translation": "Не ўдалося захаваць уліковы запіс." + }, + { + "id": "app.user.promote_guest.user_update.app_error", + "translation": "Не ўдалося абнавіць карыстальніка." + }, + { + "id": "app.user.permanentdeleteuser.internal_error", + "translation": "Немагчыма выдаліць карыстальніка." + }, + { + "id": "app.user.permanent_delete.app_error", + "translation": "Не атрымалася выдаліць існуючы ўліковы запіс." + }, + { + "id": "app.user.missing_account.const", + "translation": "Карыстальнік не знойдзены." + }, + { + "id": "app.user.get_users_batch_for_indexing.get_users.app_error", + "translation": "Не атрымалася атрымаць пакет карыстачоў для індэксацыі." + }, + { + "id": "app.user.get_unread_count.app_error", + "translation": "Не атрымалася атрымаць колькасць непрачытаных паведамленняў для карыстальніка." + }, + { + "id": "app.user.get_total_users_count.app_error", + "translation": "Не ўдалося падлічыць карыстальнікаў." + }, + { + "id": "app.user.get_threads_for_user.not_found", + "translation": "Абмеркаванне карыстальніка не існуе ці не адсочваецца" + }, + { + "id": "app.user.get_threads_for_user.app_error", + "translation": "Не ўдалося атрымаць абмеркавання карыстальніка" + }, + { + "id": "app.user.get_thread_membership_for_user.not_found", + "translation": "Удзел карыстальніка ў абмеркаванні не існуе" + }, + { + "id": "app.user.get_thread_membership_for_user.app_error", + "translation": "Не ўдалося атрымаць удзел у абмеркаванні карыстальніка" + }, + { + "id": "app.user.get_recently_active_users.app_error", + "translation": "Адбылася памылка пры пошуку нядаўна актыўных карыстальнікаў." + }, + { + "id": "app.user.get_profiles.app_error", + "translation": "Узнікла памылка пры пошуку карыстацкіх профіляў." + }, + { + "id": "app.user.get_profile_by_group_channel_ids_for_user.app_error", + "translation": "Узнікла памылка пры пошуку карыстацкіх профіляў." + }, + { + "id": "app.user.get_new_users.app_error", + "translation": "Адбылася памылка пры пошуку новых карыстальнікаў." + }, + { + "id": "app.user.get_known_users.get_users.app_error", + "translation": "Немагчыма атрымаць вядомых карыстальнікаў з базы даных." + }, + { + "id": "app.user.get_by_username.app_error", + "translation": "Не атрымалася знайсці сапраўдны ўліковы запіс з адпаведным імем карыстальніка для гэтай каманды. Гэтая каманда можа патрабаваць запрашэння ад уладальніка каманды для далучэння." + }, + { + "id": "app.user.get_by_auth.other.app_error", + "translation": "Узнікла памылка пры спробе знайсці ўліковы запіс па тыпе аўтэнтыфікацыі." + }, + { + "id": "app.user.get_by_auth.missing_account.app_error", + "translation": "Не атрымалася знайсці сапраўдны ўліковы запіс з адпаведным тыпам аўтэнтыфікацыі для гэтай каманды. Гэтая каманда можа патрабаваць запрашэння ад уладальніка каманды для далучэння." + }, + { + "id": "app.user.get.app_error", + "translation": "Узнікла памылка пошуку ўліковага запісу." + }, + { + "id": "app.user.demote_user_to_guest.user_update.app_error", + "translation": "Не ўдалося абнавіць карыстальніка." + }, + { + "id": "app.user.convert_bot_to_user.app_error", + "translation": "Немагчыма пераўтварыць робата ў карыстальніка." + }, + { + "id": "app.user.clear_all_custom_role_assignments.select.app_error", + "translation": "Не ўдалося атрымаць карыстальнікаў." + }, + { + "id": "app.user.analytics_get_inactive_users_count.app_error", + "translation": "Мы не змаглі палічыць неактыўных карыстальнікаў." + }, + { + "id": "app.user.analytics_daily_active_users.app_error", + "translation": "Не атрымалася атрымаць актыўных карыстальнікаў на працягу запытанага перыяду." + }, + { + "id": "app.upload.upload_data.update.app_error", + "translation": "Не ўдалося абнавіць сесію загрузкі." + }, + { + "id": "app.upload.upload_data.save.app_error", + "translation": "Не атрымалася захаваць інфармацыю аб файле." + }, + { + "id": "app.upload.upload_data.read_file.app_error", + "translation": "Не ўдалося прачытаць файл." + }, + { + "id": "app.upload.upload_data.move_file.app_error", + "translation": "Не атрымалася перамясціць загружаны файл." + }, + { + "id": "app.upload.upload_data.large_image.app_error", + "translation": "Памер файла {{.Filename}} ({{.Width}} на {{.Height}} пікселяў) перавышае ліміты." + }, + { + "id": "app.upload.upload_data.first_part_too_small.app_error", + "translation": "Не атрымалася загрузіць дадзеныя. Першая частка павінна быць прынамсі {{.Памер}} байт." + }, + { + "id": "app.upload.upload_data.concurrent.app_error", + "translation": "Не ўдалося загрузіць дадзеныя з некалькіх запытаў." + }, + { + "id": "app.upload.run_plugins_hook.rejected", + "translation": "Не атрымалася загрузіць файл {{.Filename}}. Адхілена плагінам: {{.Reason}}" + }, + { + "id": "app.upload.run_plugins_hook.move_fail", + "translation": "Не ўдалося перамясціць файл." + }, + { + "id": "app.upload.get_for_user.app_error", + "translation": "Не ўдалося атрымаць загружанае для карыстальніка." + }, + { + "id": "app.upload.get.app_error", + "translation": "Не ўдалося атрымаць загрузкі." + }, + { + "id": "app.upload.create.save.app_error", + "translation": "Не ўдалося захаваць загружанае." + }, + { + "id": "app.upload.create.incorrect_channel_id.app_error", + "translation": "Немагчыма загрузіць на ўказаны канал." + }, + { + "id": "app.upload.create.cannot_upload_to_deleted_channel.app_error", + "translation": "Немагчыма стварыць пост у выдаленым канале." + }, + { + "id": "app.terms_of_service.get.no_rows.app_error", + "translation": "Умовы выкарыстання не знойдзены." + }, + { + "id": "app.terms_of_service.get.app_error", + "translation": "Не ўдалося атрымаць Умовы выкарыстання." + }, + { + "id": "app.terms_of_service.create.existing.app_error", + "translation": "Не павінен выклікаць \"захаванне\" для дзейных Умоў выкарыстання." + }, + { + "id": "app.terms_of_service.create.app_error", + "translation": "Не ўдалося захаваць умовы выкарыстання." + }, + { + "id": "app.team.user_belongs_to_teams.app_error", + "translation": "Немагчыма вызначыць, ці належыць карыстач да спісу каманд." + }, + { + "id": "app.team.update.updating.app_error", + "translation": "Узнікла памылка абнаўлення каманды." + }, + { + "id": "app.team.update.find.app_error", + "translation": "Не ўдалося знайсці існуючую каманду для абнаўлення." + }, + { + "id": "app.team.search_private_team.app_error", + "translation": "Узнікла праблема пры пошуку прыватных каманд." + }, + { + "id": "app.team.search_open_team.app_error", + "translation": "Узнікла праблема пры пошуку адкрытых каманд." + }, + { + "id": "app.team.search_all_team.app_error", + "translation": "Узнікла праблема пры пошуку каманд." + }, + { + "id": "app.team.save_member.save.app_error", + "translation": "Не ўдалося захаваць удзельніка каманды." + }, + { + "id": "app.team.save.existing.app_error", + "translation": "Неабходна выклікаць абнаўленне для існуючай каманды." + }, + { + "id": "app.team.save.app_error", + "translation": "Не ўдалося захаваць каманду." + }, + { + "id": "app.team.reset_all_team_schemes.app_error", + "translation": "Мы не змаглі скінуць схемы каманды." + }, + { + "id": "app.team.rename_team.name_occupied", + "translation": "Немагчыма перайменаваць каманду, імя ўжо выкарыстоўваецца." + }, + { + "id": "app.team.remove_member.app_error", + "translation": "Не атрымалася выдаліць удзельніка каманды." + }, + { + "id": "app.team.permanentdeleteteam.internal_error", + "translation": "Немагчыма выдаліць каманду." + }, + { + "id": "app.team.permanent_delete.app_error", + "translation": "Не атрымалася выдаліць існуючую каманду." + }, + { + "id": "app.team.migrate_team_members.update.app_error", + "translation": "Не ўдалося абнавіць удзельніка каманды." + }, + { + "id": "app.team.join_user_to_team.save_member.max_accounts.app_error", + "translation": "Немагчыма стварыць новы ўдзел у камандзе, таму што каманда дасягнула мяжы ўдзельнікаў" + }, + { + "id": "app.team.join_user_to_team.save_member.conflict.app_error", + "translation": "Немагчыма стварыць новы ўдзел у камандзе, таму што ён ужо існуе" + }, + { + "id": "app.team.join_user_to_team.save_member.app_error", + "translation": "Немагчыма стварыць новы ўдзел у камандзе" + }, + { + "id": "app.team.join_user_to_team.max_accounts.app_error", + "translation": "Гэтая каманда дасягнула максімальнай колькасці дазволеных уліковых запісаў. Звяжыцеся з сістэмным адміністратарам для павелічэння ліміту уліковых запісаў." + }, + { + "id": "app.team.invite_token.group_constrained.error", + "translation": "Немагчыма далучыцца да кіраванай групамі камандзе па токене." + }, + { + "id": "app.team.invite_id.group_constrained.error", + "translation": "Немагчыма далучыцца да кіруемай групамі камандзе па запрашэнні." + }, + { + "id": "app.team.get_user_team_ids.app_error", + "translation": "Немагчыма атрымаць спіс каманд, у якіх складаецца карыстач." + }, + { + "id": "app.team.get_unread.app_error", + "translation": "Не атрымалася атрымаць спіс непрачытаных паведамленняў для каманд." + }, + { + "id": "app.team.get_members_by_ids.app_error", + "translation": "Не ўдалося атрымаць удзельнікаў каманды." + }, + { + "id": "app.team.get_members.app_error", + "translation": "Не ўдалося атрымаць удзельнікаў каманды." + }, + { + "id": "app.team.get_member_count.app_error", + "translation": "Не ўдалося падлічыць удзельнікаў каманды." + }, + { + "id": "app.team.get_member.missing.app_error", + "translation": "Не знойдзена ніводнага ўдзельніка каманды па дадзеных ідэнтыфікатарах карыстальніка і каманды." + }, + { + "id": "app.team.get_member.app_error", + "translation": "Не ўдалося атрымаць удзельніка каманды." + }, + { + "id": "app.team.get_common_team_ids_for_users.app_error", + "translation": "Не атрымалася атрымаць ID каманды." + }, + { + "id": "app.team.get_by_scheme.app_error", + "translation": "Немагчыма атрымаць каналы для прадстаўленай схемы." + }, + { + "id": "app.team.get_by_name.missing.app_error", + "translation": "Не ўдалося знайсці існуючую каманду." + }, + { + "id": "app.team.get_by_name.app_error", + "translation": "Не ўдалося знайсці існуючую каманду." + }, + { + "id": "app.team.get_by_invite_id.finding.app_error", + "translation": "Не ўдалося знайсці існуючую каманду." + }, + { + "id": "app.team.get_all_team_listing.app_error", + "translation": "Не ўдалося атрымаць усе каманды." + }, + { + "id": "app.team.get_all_private_team_listing.app_error", + "translation": "Не ўдалося атрымаць усе прыватныя каманды." + }, + { + "id": "app.team.get_all.app_error", + "translation": "Не ўдалося атрымаць усе каманды." + }, + { + "id": "app.team.get_active_member_count.app_error", + "translation": "Не ўдалося падлічыць удзельнікаў каманды." + }, + { + "id": "app.team.get.finding.app_error", + "translation": "Узнікла памылка з выяўленнем каманды." + }, + { + "id": "app.team.get.find.app_error", + "translation": "Не ўдалося знайсці існуючую каманду." + }, + { + "id": "app.team.clear_all_custom_role_assignments.select.app_error", + "translation": "Не ўдалося атрымаць удзельнікаў каманды." + }, + { + "id": "app.team.analytics_team_count.app_error", + "translation": "Не ўдалося падлічыць каманды." + }, + { + "id": "app.system_install_date.parse_int.app_error", + "translation": "Не ўдалося разабраць дату ўстаноўкі." + }, + { + "id": "app.system.warn_metric.store.app_error", + "translation": "Не ўдалося захаваць значэнне для {{.WarnMetricName}}" + }, + { + "id": "app.system.save.app_error", + "translation": "Узнікла памылка пры захаванні ўласцівасці сістэмы." + }, + { + "id": "app.system.permanent_delete_by_name.app_error", + "translation": "Мы не можам канчаткова выдаліць запіс сістэмнай табліцы." + }, + { + "id": "app.system.get_by_name.app_error", + "translation": "Немагчыма знайсці сістэмную зменную." + }, + { + "id": "app.system.get.app_error", + "translation": "Узнікла памылка пры пошуку ўласцівасцей сістэмы." + }, + { + "id": "app.submit_interactive_dialog.json_error", + "translation": "Выяўлена памылка кадавання JSON для інтэрактыўнага дыялогу." + }, + { + "id": "app.status.get.missing.app_error", + "translation": "Не існуе запісу для гэтага статусу." + }, + { + "id": "app.status.get.app_error", + "translation": "Узнікла памылка пры атрыманні статуту." + }, + { + "id": "app.sharedchannel.dm_channel_creation.internal_error", + "translation": "Узнікла памылка пры стварэнні прамога агульнага канала." + }, + { + "id": "app.session.update_device_id.app_error", + "translation": "Немагчыма абнавіць ідэнтыфікатар прылады." + }, + { + "id": "app.session.save.existing.app_error", + "translation": "Немагчыма абнавіць існуючую сесію." + }, + { + "id": "app.session.save.app_error", + "translation": "Немагчыма захаваць сесію." + }, + { + "id": "app.session.remove_all_sessions_for_team.app_error", + "translation": "Немагчыма выдаліць усе сесіі." + }, + { + "id": "app.session.remove.app_error", + "translation": "Немагчыма выдаліць сесію." + }, + { + "id": "app.session.permanent_delete_sessions_by_user.app_error", + "translation": "Немагчыма выдаліць усе сеансы для карыстальніка." + }, + { + "id": "app.session.get_sessions.app_error", + "translation": "Мы выявілі памылку пры пошуку карыстацкіх сесій." + }, + { + "id": "app.session.get.app_error", + "translation": "Мы знайшлі памылку пры пошуку сесіі." + }, + { + "id": "app.session.analytics_session_count.app_error", + "translation": "Немагчыма падлічыць сесіі." + }, + { + "id": "app.schemes.is_phase_2_migration_completed.not_completed.app_error", + "translation": "Гэтая канчатковая кропка API недаступная, паколькі неабходныя міграцыі яшчэ не завершаны." + }, + { + "id": "app.scheme.save.invalid_scheme.app_error", + "translation": "Указаная схема несапраўдная." + }, + { + "id": "app.scheme.save.app_error", + "translation": "Не ўдалося стварыць схему." + }, + { + "id": "app.scheme.permanent_delete_all.app_error", + "translation": "Мы не змаглі канчаткова выдаліць схемы." + }, + { + "id": "app.scheme.get.app_error", + "translation": "Не ўдалося атрымаць схему." + }, + { + "id": "app.scheme.delete.app_error", + "translation": "Немагчыма выдаліць гэтую схему." + }, + { + "id": "app.save_config.app_error", + "translation": "Адбылася памылка пры захаванні канфігурацыі." + }, + { + "id": "app.role.save.invalid_role.app_error", + "translation": "Роля не была сапраўднай." + }, + { + "id": "app.role.save.insert.app_error", + "translation": "Немагчыма захаваць новую ролю." + }, + { + "id": "app.role.permanent_delete_all.app_error", + "translation": "Мы не змаглі канчаткова выдаліць усе ролі." + }, + { + "id": "app.role.get_by_names.app_error", + "translation": "Немагчыма атрымаць ролі." + }, + { + "id": "app.role.get_by_name.app_error", + "translation": "Немагчыма атрымаць ролю." + }, + { + "id": "app.role.get.app_error", + "translation": "Немагчыма атрымаць ролю." + }, + { + "id": "app.role.check_roles_exist.role_not_found", + "translation": "Прадстаўленая роля не існуе" + }, + { + "id": "app.recover.save.app_error", + "translation": "Не ўдалося захаваць токен." + }, + { + "id": "app.recover.delete.app_error", + "translation": "Не атрымалася выдаліць токен." + }, + { + "id": "app.reaction.save.save.app_error", + "translation": "Не ўдалося захаваць рэакцыю." + }, + { + "id": "app.reaction.get_for_post.app_error", + "translation": "Не атрымалася атрымаць рэакцыі для дадзенага паведамлення." + }, + { + "id": "app.reaction.delete_all_with_emoji_name.get_reactions.app_error", + "translation": "Не атрымалася атрымаць усе рэакцыі з дадзеным імем смайла." + }, + { + "id": "app.reaction.bulk_get_for_post_ids.app_error", + "translation": "Не атрымалася атрымаць рэакцыі для дадзенага паведамлення." + }, + { + "id": "app.preference.save.updating.app_error", + "translation": "Узнікла памылка пры абнаўленні налад." + }, + { + "id": "app.preference.permanent_delete_by_user.app_error", + "translation": "Узнікла памылка пры выдаленні налад." + }, + { + "id": "app.preference.get_category.app_error", + "translation": "Узнікла памылка пры пошуку настроек." + }, + { + "id": "app.preference.get_all.app_error", + "translation": "Узнікла памылка пры пошуку настроек." + }, + { + "id": "app.preference.get.app_error", + "translation": "Узнікла памылка пры пошуку настроек." + }, + { + "id": "app.preference.delete.app_error", + "translation": "Узнікла памылка пры выдаленні налад." + }, + { + "id": "app.post.update.app_error", + "translation": "Немагчыма абнавіць паведамленне." + }, + { + "id": "app.post.save.existing.app_error", + "translation": "Вы не можаце абнавіць існуючае паведамленне." + }, + { + "id": "app.post.save.app_error", + "translation": "Немагчыма захаваць паведамленне." + }, + { + "id": "app.post.permanent_delete_by_user.app_error", + "translation": "Не атрымалася абраць для выдалення пасты карыстальніка." + }, + { + "id": "app.post.permanent_delete_by_channel.app_error", + "translation": "Немагчыма выдаліць паведамленні па канале." + }, + { + "id": "app.post.overwrite.app_error", + "translation": "Не ўдалося змяніць паведамленне." + }, + { + "id": "app.post.get_root_posts.app_error", + "translation": "Не ўдалося атрымаць паведамленні для канала." + }, + { + "id": "app.post.get_posts_since.app_error", + "translation": "Не ўдалося атрымаць паведамленні для канала." + }, + { + "id": "app.post.get_posts_created_at.app_error", + "translation": "Не ўдалося атрымаць паведамленні для канала." + }, + { + "id": "app.post.get_posts_batch_for_indexing.get.app_error", + "translation": "Немагчыма атрымаць пакет паведамленняў для індэксацыі." + }, + { + "id": "app.post.get_posts_around.get.app_error", + "translation": "Не ўдалося атрымаць паведамленні для канала." + }, + { + "id": "app.post.get_posts.app_error", + "translation": "Перавышана абмежаванне колькасці старонак." + }, + { + "id": "app.post.get_post_id_around.app_error", + "translation": "Немагчыма атрымаць паведамленне на працягу вызначанага часу." + }, + { + "id": "app.post.get_post_after_time.app_error", + "translation": "Немагчыма атрымаць паведамленне пасля заканчэння часу." + }, + { + "id": "app.post.get_flagged_posts.app_error", + "translation": "Не ўдалося атрымаць адзначаныя сцягам паведамленні." + }, + { + "id": "app.post.get_files_batch_for_indexing.get.app_error", + "translation": "Не ўдалося атрымаць пакет файлаў для індэксацыі." + }, + { + "id": "app.post.get_direct_posts.app_error", + "translation": "Не ўдалося атрымаць асабістыя паведамленні." + }, + { + "id": "app.post.get.app_error", + "translation": "Не ўдалося атрымаць паведамленне." + }, + { + "id": "app.post.delete.app_error", + "translation": "Немагчыма выдаліць паведамленне." + }, + { + "id": "app.post.analytics_user_counts_posts_by_day.app_error", + "translation": "Немагчыма атрымаць колькасць карыстальнікаў з паведамленнямі." + }, + { + "id": "app.post.analytics_posts_count_by_day.app_error", + "translation": "Немагчыма атрымаць колькасць паведамленняў па днях." + }, + { + "id": "app.post.analytics_posts_count.app_error", + "translation": "Немагчыма атрымаць колькасць паведамленняў." + }, + { + "id": "app.plugin_store.save.app_error", + "translation": "Не атрымалася захаваць ці абнавіць значэнне ключа плагіна." + }, + { + "id": "app.plugin_store.list.app_error", + "translation": "Немагчыма пералічыць усе ключы плагінаў." + }, + { + "id": "app.plugin_store.get.app_error", + "translation": "Не атрымалася атрымаць значэнне ключа плагіна." + }, + { + "id": "app.plugin_store.delete.app_error", + "translation": "Не атрымалася выдаліць значэнне ключа плагіна." + }, + { + "id": "app.plugin.write_file.saving.app_error", + "translation": "Адбылася памылка пры захаванні файла." + }, + { + "id": "app.plugin.write_file.read.app_error", + "translation": "Адбылася памылка пры чытанні файла." + }, + { + "id": "app.plugin.webapp_bundle.app_error", + "translation": "Немагчыма згенераваць пакет для вэб-часткі плагіна (камплекта вэб-прыкладанні)." + }, + { + "id": "app.plugin.upload_disabled.app_error", + "translation": "Убудовы і/або загрузка плагінаў адключаныя." + }, + { + "id": "app.plugin.sync.read_local_folder.app_error", + "translation": "Памылка чытання лакальнай тэчкі плагінаў." + }, + { + "id": "app.plugin.sync.list_filestore.app_error", + "translation": "Памылка чытання файлаў з тэчкі плагінаў у файлавым сховішчы." + }, + { + "id": "app.plugin.store_signature.app_error", + "translation": "Не атрымалася захаваць сігнатуру плагіна ў бягучае файлавае сховішча." + }, + { + "id": "app.plugin.store_bundle.app_error", + "translation": "Не атрымалася захаваць плягін у бягучае файлавае сховішча." + }, + { + "id": "app.notification.body.thread.title", + "translation": "{{.SenderName}} адказаў у абмеркаванні" + }, + { + "id": "app.notification.body.mention.title", + "translation": "{{.SenderName}} згадаў Вас у паведамленні" + }, + { + "id": "app.notification.body.mention.subTitle", + "translation": "Пакуль Вас не было, {{.SenderName}} згадаў Вас у канале {{.ChannelName}}." + }, + { + "id": "app.notification.body.group.title", + "translation": "{{.SenderName}} даслаў Вам новае паведамленне" + }, + { + "id": "app.notification.body.group.subTitle", + "translation": "Пакуль вас не было, {{.SenderName}} даслаў паведамленне Вашай групе." + }, + { + "id": "app.notification.body.dm.title", + "translation": "{{.SenderName}} адправіў вам новае паведамленне" + }, + { + "id": "app.notification.body.dm.subTitle", + "translation": "Пакуль вас не было, {{.SenderName}} даслаў вам новае асабістае паведамленне." + }, + { + "id": "app.license.generate_renewal_token.bad_license", + "translation": "Гэты тып ліцэнзіі не падтрымлівае генерацыю токенаў абнаўлення" + }, + { + "id": "app.license.generate_renewal_token.app_error", + "translation": "Не ўдалося згенераваць новы токен абнаўлення." + }, + { + "id": "app.job.update.app_error", + "translation": "Не ўдалося абнавіць заданне." + }, + { + "id": "app.job.save.app_error", + "translation": "Не ўдалося захаваць заданне." + }, + { + "id": "app.job.get_newest_job_by_status_and_type.app_error", + "translation": "Немагчыма атрымаць самае новае заданне па статуце і тыпу." + }, + { + "id": "app.job.get_count_by_status_and_type.app_error", + "translation": "Немагчыма атрымаць колькасць заданняў па статуце і тыпе." + }, + { + "id": "app.job.get_all.app_error", + "translation": "Не ўдалося атрымаць заданні." + }, + { + "id": "app.job.get.app_error", + "translation": "Не ўдалося атрымаць заданне." + }, + { + "id": "app.job.download_export_results_not_enabled", + "translation": "DownloadExportResults у config.json мае значэнне false. Калі ласка, усталюйце значэнне true, каб загрузіць вынікі гэтага задання." + }, + { + "id": "app.import.validate_user_teams_import_data.team_name_missing.error", + "translation": "Прапушчана імя каманды ў \"Удзел карыстальнікаў у камандзе\"." + }, + { + "id": "app.import.validate_user_teams_import_data.invalid_team_theme.error", + "translation": "Няправільная тэма каманды для карыстальніка" + }, + { + "id": "app.import.validate_user_teams_import_data.invalid_roles.error", + "translation": "Няправільныя ролі ў \"Удзел карыстальнікаў у камандзе\"." + }, + { + "id": "app.import.validate_user_import_data.username_missing.error", + "translation": "Прапушчана неабходная ўласцівасць user:username." + }, + { + "id": "app.import.validate_user_import_data.username_invalid.error", + "translation": "Няправільнае імя карыстальніка." + }, + { + "id": "app.import.validate_user_import_data.roles_invalid.error", + "translation": "Няправільныя ролі карыстальніка." + }, + { + "id": "app.import.validate_user_import_data.position_length.error", + "translation": "Размяшчэнне карыстальніка занадта доўгае." + }, + { + "id": "app.import.validate_user_import_data.password_length.error", + "translation": "Пароль карыстальніка мае недапушчальную даўжыню." + }, + { + "id": "app.import.validate_user_import_data.notify_props_mobile_push_status_invalid.error", + "translation": "Няправільная ўласцівасць Mobile Push Status Notify для карыстальніка." + }, + { + "id": "app.import.validate_user_import_data.notify_props_mobile_invalid.error", + "translation": "Няправільнае значэнне Mobile Notify Prop для карыстальніка." + }, + { + "id": "app.import.validate_user_import_data.notify_props_email_invalid.error", + "translation": "Няправільнае значэнне Email Notify Prop для карыстальніка." + }, + { + "id": "app.import.validate_user_import_data.notify_props_desktop_sound_invalid.error", + "translation": "Няправільнае значэнне Desktop Sound Notify Prop для карыстальніка." + }, + { + "id": "app.import.validate_user_import_data.notify_props_desktop_invalid.error", + "translation": "Няправільнае значэнне Desktop Notify Prop для карыстальніка." + }, + { + "id": "app.import.validate_user_import_data.notify_props_comments_trigger_invalid.error", + "translation": "Няправільнае значэнне Comments Prop для карыстальніка." + }, + { + "id": "app.import.validate_user_import_data.notify_props_channel_trigger_invalid.error", + "translation": "Няверныя Channel Trigger Notify Prop для карыстальніка." + }, + { + "id": "app.import.validate_user_import_data.nickname_length.error", + "translation": "Псеўданім карыстальніка занадта доўгі." + }, + { + "id": "app.import.validate_user_import_data.last_name_length.error", + "translation": "Прозвішча карыстальніка занадта доўгае." + }, + { + "id": "app.import.validate_user_import_data.first_name_length.error", + "translation": "Імя карыстальніка занадта доўгае." + }, + { + "id": "app.import.validate_user_import_data.email_missing.error", + "translation": "Прапушчана абавязковая ўласцівасць user: email." + }, + { + "id": "app.import.validate_user_import_data.email_length.error", + "translation": "E-Mail карыстальніка мае няправільную даўжыню." + }, + { + "id": "app.import.validate_user_import_data.auth_data_length.error", + "translation": "Структура AuthData карыстальніка занадта вялікая." + }, + { + "id": "app.import.validate_user_import_data.auth_data_and_service_dependency.error", + "translation": "Карыстальніцкія AuthService і AuthData з'яўляюцца ўзаемна ўключальнымі." + }, + { + "id": "app.import.validate_user_import_data.auth_data_and_password.error", + "translation": "Карыстальніцкія AuthData і Пароль узаемавыключальныя." + }, + { + "id": "app.import.validate_user_import_data.advanced_props_show_unread_section.error", + "translation": "Няправільны параметр паказу непрачытанай часткі для карыстальніка" + }, + { + "id": "app.import.validate_user_import_data.advanced_props_formatting.error", + "translation": "Няправільны параметр фарматавання запісу для карыстальніка" + }, + { + "id": "app.import.validate_user_import_data.advanced_props_feature_markdown_preview.error", + "translation": "Няправільныя параметры папярэдняга прагляду markdown для карыстальніка" + }, + { + "id": "app.import.validate_user_import_data.advanced_props_email_interval.error", + "translation": "Няправільны інтэрвал дазавання электроннай пошты для карыстальніка" + }, + { + "id": "app.import.validate_user_channels_import_data.invalid_roles.error", + "translation": "Няправільныя ролі ў \"Удзел карыстальнікаў на каналах\"." + }, + { + "id": "app.import.validate_user_channels_import_data.invalid_notify_props_mobile.error", + "translation": "Няправільныя ўласцівасці апавяшчэння мабільных для ўдзелу карыстальніка ў каналах." + }, + { + "id": "app.import.validate_user_channels_import_data.invalid_notify_props_mark_unread.error", + "translation": "Няправільныя ўласцівасці апавяшчэння \"прачытана\" для ўдзелу карыстальніка ў каналах." + }, + { + "id": "app.import.validate_user_channels_import_data.invalid_notify_props_desktop.error", + "translation": "Няправільныя ўласцівасці апавяшчэнняў на працоўным стале для карыстацкага ўдзелу ў каналах." + }, + { + "id": "app.import.validate_user_channels_import_data.channel_name_missing.error", + "translation": "Прапушчана імя канала ў \"Удзел карыстальнікаў у каналах\"." + }, + { + "id": "app.import.validate_team_import_data.type_missing.error", + "translation": "Прапушчана неабходная ўласцівасць team:type." + }, + { + "id": "app.import.validate_team_import_data.type_invalid.error", + "translation": "Няправільны тып каманды." + }, + { + "id": "app.import.validate_team_import_data.scheme_invalid.error", + "translation": "Недапушчальнае імя схемы для каманды." + }, + { + "id": "app.import.validate_team_import_data.name_reserved.error", + "translation": "Імя каманды змяшчае зарэзерваваныя словы." + }, + { + "id": "app.import.validate_team_import_data.name_missing.error", + "translation": "Прапушчана неабходная ўласцівасць team: name." + }, + { + "id": "app.import.validate_team_import_data.name_length.error", + "translation": "Імя каманды занадта доўгае." + }, + { + "id": "app.import.validate_team_import_data.name_characters.error", + "translation": "Імя каманды змяшчае недапушчальныя знакі." + }, + { + "id": "app.import.validate_team_import_data.display_name_missing.error", + "translation": "Адсутнічае патрэбнае поле для каманды: display_name." + }, + { + "id": "app.import.validate_team_import_data.display_name_length.error", + "translation": "Уласцівасць Team display_name не знаходзіцца ў межах дапушчальных абмежаванняў даўжыні." + }, + { + "id": "app.import.validate_team_import_data.description_length.error", + "translation": "Апісанне каманды занадта доўгае." + }, + { + "id": "app.import.validate_scheme_import_data.wrong_roles_for_scope.error", + "translation": "Няправільныя ролі былі прадстаўлены для схемы з гэтай вобласцю." + }, + { + "id": "app.import.validate_scheme_import_data.null_scope.error", + "translation": "Патрабуецца задаць вобласць ахопу Схемы." + }, + { + "id": "app.import.validate_scheme_import_data.display_name_invalid.error", + "translation": "Недапушчальнае імя для адлюстравання схемы." + }, + { + "id": "app.import.validate_role_import_data.invalid_permission.error", + "translation": "Недапушчальнае дазвол ролі." + }, + { + "id": "app.import.validate_role_import_data.display_name_invalid.error", + "translation": "Недапушчальнае імя для адлюстравання ролі." + }, + { + "id": "app.import.validate_reply_import_data.user_missing.error", + "translation": "Адсутнічае неабходнае значэнне для Reply: User." + }, + { + "id": "app.import.validate_reply_import_data.message_missing.error", + "translation": "Адсутнічае неабходнае значэнне для Reply: Message." + }, + { + "id": "app.import.validate_reply_import_data.message_length.error", + "translation": "Значэнне Message у Reply даўжэй максімальна дазволены даўжыні." + }, + { + "id": "app.import.validate_reply_import_data.create_at_zero.error", + "translation": "Значэнне CreateAt у Reply павінна быць не нулявым." + }, + { + "id": "app.import.validate_reply_import_data.create_at_missing.error", + "translation": "Адсутнічае неабходнае значэнне для Reply: create_at." + }, + { + "id": "app.import.validate_reaction_import_data.user_missing.error", + "translation": "Адсутнічае неабходнае значэнне для Reaction: User." + }, + { + "id": "app.import.validate_reaction_import_data.emoji_name_missing.error", + "translation": "Адсутнічае неабходнае значэнне для Reaction: EmojiName." + }, + { + "id": "app.import.validate_reaction_import_data.emoji_name_length.error", + "translation": "Значэнне EmojiName у Reaction даўжэй максімальна дазволенай даўжыні." + }, + { + "id": "app.import.validate_reaction_import_data.create_at_zero.error", + "translation": "Значэнне CreateAt у Reaction павінна быць не нулявым." + }, + { + "id": "app.import.validate_reaction_import_data.create_at_missing.error", + "translation": "Адсутнічае неабходнае значэнне для Reaction: create_at." + }, + { + "id": "app.import.validate_reaction_import_data.create_at_before_parent.error", + "translation": "Значэнне CreateAt у Reaction павінна быць больш чым бацькоўскі CreateAt у Post." + }, + { + "id": "app.import.validate_post_import_data.user_missing.error", + "translation": "Адсутнічае неабходнае поле для Post:User." + }, + { + "id": "app.import.validate_post_import_data.team_missing.error", + "translation": "Адсутнічае неабходнае поле для Post: Team." + }, + { + "id": "app.import.validate_post_import_data.props_too_large.error", + "translation": "Уласцівасці паведамлення даўжэй максімальна дапушчальнай даўжыні." + }, + { + "id": "app.import.validate_post_import_data.message_missing.error", + "translation": "Адсутнічае неабходнае поле для Post: Message." + }, + { + "id": "app.import.validate_post_import_data.message_length.error", + "translation": "Post Message даўжэйшая, чым максімальна дазволеная даўжыня." + }, + { + "id": "app.import.validate_post_import_data.create_at_zero.error", + "translation": "Значэнне CreateAt у Post павінна быць не нулявым." + }, + { + "id": "app.import.validate_post_import_data.create_at_missing.error", + "translation": "Адсутнічае патрэбнае поле для Post: create_at." + }, + { + "id": "app.import.validate_post_import_data.channel_missing.error", + "translation": "Адсутнічае неабходнае поле для Post: Channel." + }, + { + "id": "app.import.validate_emoji_import_data.name_missing.error", + "translation": "Поле \"імя імпартуемага смайліка\" прапушчана або пустое." + }, + { + "id": "app.import.validate_emoji_import_data.image_missing.error", + "translation": "Імпарт emoji - выява адсутнічае або поле пустое." + }, + { + "id": "app.import.validate_emoji_import_data.empty.error", + "translation": "Няма дадзеных аб эмоджы для імпарту." + }, + { + "id": "app.import.validate_direct_post_import_data.user_missing.error", + "translation": "Адсутнічае неабходнае поле для post:" + }, + { + "id": "app.import.validate_direct_post_import_data.unknown_flagger.error", + "translation": "Толькі ўдзельнікі канала могуць пазначаць сцягамі прамое паведамленне ў гэтым канале. \"{{.Username}}\" не ўдзельнік." + }, + { + "id": "app.import.validate_direct_post_import_data.message_missing.error", + "translation": "Адсутнічае неабходнае поле для post: message" + }, + { + "id": "app.import.validate_direct_post_import_data.message_length.error", + "translation": "Паведамленне занадта доўгае" + }, + { + "id": "app.import.validate_direct_post_import_data.create_at_zero.error", + "translation": "CreateAt павінна быць больш" + }, + { + "id": "app.import.validate_direct_post_import_data.create_at_missing.error", + "translation": "Адсутнічае патрэбнае поле для post: create_at" + }, + { + "id": "app.import.validate_direct_post_import_data.channel_members_too_many.error", + "translation": "У спісе ўдзельнікаў асабістага канала занадта шмат элементаў" + }, + { + "id": "app.import.validate_direct_post_import_data.channel_members_too_few.error", + "translation": "У спісе ўдзельнікаў асабістага канала занадта мала элементаў" + }, + { + "id": "app.import.validate_direct_post_import_data.channel_members_required.error", + "translation": "Адсутнічае патрэбнае поле для post: channel_members" + }, + { + "id": "app.import.validate_direct_channel_import_data.unknown_favoriter.error", + "translation": "Бягучы канал можа быць дададзены да выбранага толькі ўдзельнікамі каманды. \"{{.Username}}\" не з'яўляецца ўдзельнікам гэтай каманды." + }, + { + "id": "app.import.validate_direct_channel_import_data.members_too_many.error", + "translation": "У спісе ўдзельнікаў асабістага канала занадта шмат элементаў" + }, + { + "id": "app.import.validate_direct_channel_import_data.members_too_few.error", + "translation": "У спісе ўдзельнікаў асабістага канала занадта мала элементаў" + }, + { + "id": "app.import.validate_direct_channel_import_data.members_required.error", + "translation": "Прапушчана абавязковая ўласцівасць direct channel: members" + }, + { + "id": "app.import.validate_direct_channel_import_data.header_length.error", + "translation": "Загаловак асабістага канала занадта доўгі" + }, + { + "id": "app.import.validate_channel_import_data.type_missing.error", + "translation": "Прапушчана абавязковая ўласцівасць channel:type." + }, + { + "id": "app.import.validate_channel_import_data.type_invalid.error", + "translation": "Няправільны тып канала." + }, + { + "id": "app.import.validate_channel_import_data.team_missing.error", + "translation": "Прапушчана абавязковая ўласцівасць channel: team" + }, + { + "id": "app.import.validate_channel_import_data.scheme_invalid.error", + "translation": "Недапушчальнае імя схемы для канала." + }, + { + "id": "app.import.validate_channel_import_data.purpose_length.error", + "translation": "Загаловак канала занадта доўгі." + }, + { + "id": "app.import.validate_channel_import_data.name_missing.error", + "translation": "Прапушчана абавязковая ўласцівасць channel: name" + }, + { + "id": "app.import.validate_channel_import_data.name_length.error", + "translation": "Імя канала занадта доўгае." + }, + { + "id": "app.import.validate_channel_import_data.name_characters.error", + "translation": "Імя канала змяшчае недапушчальныя сімвалы." + }, + { + "id": "app.import.validate_channel_import_data.header_length.error", + "translation": "Даўжыня загалоўка канала занадта вялікая." + }, + { + "id": "app.import.validate_channel_import_data.display_name_length.error", + "translation": "Уласцівасць Channel display_name не знаходзіцца ў межах дапушчальных абмежаванняў даўжыні." + }, + { + "id": "app.import.process_import_data_file_version_line.invalid_version.error", + "translation": "Немагчыма прачытаць версію імпартнага файла дадзеных." + }, + { + "id": "app.import.import_user_teams.save_preferences.error", + "translation": "Не атрымалася захаваць настройкі тэмы каманды" + }, + { + "id": "app.import.import_user_teams.save_members.max_accounts.app_error", + "translation": "Немагчыма імпартаваць удзел у камандзе, таму што ў гэтай камандзе не дазволены дадатковыя ўдзельнікі" + }, + { + "id": "app.import.import_user_teams.save_members.error", + "translation": "Немагчыма імпартаваць удзел у камандзе" + }, + { + "id": "app.import.import_user_teams.save_members.conflict.app_error", + "translation": "Немагчыма імпартаваць новы ўдзел у камандзе, таму што ён ужо існуе" + }, + { + "id": "app.import.import_user_channels.save_preferences.error", + "translation": "Памылка імпарту ўдзелу карыстальніка ў каналах. Не ўдалося захаваць наладкі." + }, + { + "id": "app.import.import_user_channels.channel_not_found.error", + "translation": "Памылка імпарту карыстацкіх каналаў. Канал не знойдзены." + }, + { + "id": "app.import.import_user.save_preferences.error", + "translation": "Памылка імпарту налад карыстальніка. Не ўдалося захаваць наладкі." + }, + { + "id": "app.import.import_team.scheme_wrong_scope.error", + "translation": "Каманда павінна быць прызначаная схеме ўзроўня каманд." + }, + { + "id": "app.import.import_team.scheme_deleted.error", + "translation": "Немагчыма ўсталяваць выкарыстанне выдаленай схемы для каманды." + }, + { + "id": "app.import.import_scheme.scope_change.error", + "translation": "Масавы імпарцёр не можа змяніць вобласць дзеяння ўжо існай схемы." + }, + { + "id": "app.import.import_post.user_not_found.error", + "translation": "Памылка імпарту паведамлення. Карыстальніка з імем \"{{.Username}}\" не знойдзены." + }, + { + "id": "app.import.import_post.save_preferences.error", + "translation": "Памылка падчас імпарту паста. Не ўдалося захаваць наладкі." + }, + { + "id": "app.import.import_post.channel_not_found.error", + "translation": "Памылка імпарту паведамлення. Канал з імем \"{{.ChannelName}}\" не знойдзены." + }, + { + "id": "app.import.import_line.unknown_line_type.error", + "translation": "Радок імпарту дадзеных утрымоўвае невядомы тып \"{{.Type}}\"." + }, + { + "id": "app.import.import_line.null_user.error", + "translation": "Радок імпарту дадзеных утрымоўвае тып \"user\", але аб'ект user роўны null." + }, + { + "id": "app.import.import_line.null_team.error", + "translation": "Радок імпарту дадзеных утрымоўвае тып \"team\", але аб'ект team роўны null." + }, + { + "id": "app.import.import_line.null_scheme.error", + "translation": "Радок імпарту дадзеных утрымоўвае тып \"scheme\", але аб'ект scheme роўны null." + }, + { + "id": "app.import.import_line.null_post.error", + "translation": "Радок імпарту дадзеных утрымоўвае тып \"post\", але аб'ект post роўны null." + }, + { + "id": "app.import.import_line.null_emoji.error", + "translation": "Радок імпарту дадзеных утрымоўвае тып \"emoji\", але аб'ект post роўны null." + }, + { + "id": "app.import.import_line.null_direct_post.error", + "translation": "Радок імпарту дадзеных утрымоўвае тып \"direct_post\", але аб'ект direct_post роўны null." + }, + { + "id": "app.import.import_line.null_direct_channel.error", + "translation": "Радок імпарту дадзеных утрымоўвае тып \"direct_channel\", але direct_channel з'яўляецца null." + }, + { + "id": "app.import.import_line.null_channel.error", + "translation": "Радок імпарту дадзеных утрымоўвае тып \"channel\", але аб'ект channel роўны null." + }, + { + "id": "app.import.import_direct_post.create_group_channel.error", + "translation": "Не ўдалося атрымаць групавы канал" + }, + { + "id": "app.import.import_direct_post.create_direct_channel.error", + "translation": "Не ўдалося атрымаць прамы канал" + }, + { + "id": "app.import.import_direct_channel.update_header_failed.error", + "translation": "Не атрымалася абнавіць загаловак канала" + }, + { + "id": "app.import.import_direct_channel.create_group_channel.error", + "translation": "Не ўдалося стварыць групавы канал" + }, + { + "id": "app.import.import_direct_channel.create_direct_channel.error", + "translation": "Не ўдалося стварыць прамы канал" + }, + { + "id": "app.import.import_channel.team_not_found.error", + "translation": "Памылка імпарту канала. Каманда з імем \"{{.TeamName}}\" не знойдзена." + }, + { + "id": "app.import.import_channel.scheme_wrong_scope.error", + "translation": "Канал павінен быць прызначаны ў схеме ўзроўня канала." + }, + { + "id": "app.import.import_channel.scheme_deleted.error", + "translation": "Нельга прызначыць каналу выдаленую схему." + }, + { + "id": "app.import.get_users_by_username.some_users_not_found.error", + "translation": "Некаторыя карыстальнікі не знойдзены" + }, + { + "id": "app.import.get_teams_by_names.some_teams_not_found.error", + "translation": "Некаторыя каманды не знойдзены" + }, + { + "id": "app.import.emoji.bad_file.error", + "translation": "Памылка пры чытанні файла выявы emoji. Emoji з імем: \"{{.EmojiName}}\"" + }, + { + "id": "app.import.bulk_import.unsupported_version.error", + "translation": "Няправільная ці адсутная версія ў файле імпарту дадзеных. Упэўніцеся, што версія - гэта першы аб'ект у файле імпарту і паспрабуйце зноў." + }, + { + "id": "app.import.bulk_import.json_decode.error", + "translation": "Памылка апрацоўкі радка JSON." + }, + { + "id": "app.import.bulk_import.file_scan.error", + "translation": "Памылка чытання імпартнага файла." + }, + { + "id": "app.import.attachment.read_file_data.error", + "translation": "Не ўдалося прачытаць аттачмент пры імпарце." + }, + { + "id": "app.import.attachment.file_upload.error", + "translation": "Памылка пры загрузцы файла: \"{{.FilePath}}\"" + }, + { + "id": "app.import.attachment.bad_file.error", + "translation": "Памылка чытання файла: \"{{.FilePath}}\"" + }, + { + "id": "app.group.uniqueness_error", + "translation": "удзельнік групы ўжо існуе" + }, + { + "id": "app.group.permanent_delete_members_by_user.app_error", + "translation": "Немагчыма выдаліць удзельніка групы з UserID \"{{.UserId}}\"." + }, + { + "id": "app.group.no_rows", + "translation": "не знойдзена прыдатнай групы" + }, + { + "id": "app.group.id.app_error", + "translation": "недапушчальная ўласцівасць Id для групы." + }, + { + "id": "app.group.group_syncable_already_deleted", + "translation": "сінхранізаваная група ўжо выдалена" + }, + { + "id": "app.file_info.save.app_error", + "translation": "Не атрымалася захаваць інфармацыю аб файле." + }, + { + "id": "app.file_info.permanent_delete_by_user.app_error", + "translation": "Немагчыма выдаліць укладанні карыстальніка." + }, + { + "id": "app.file_info.get_with_options.app_error", + "translation": "Не атрымалася атрымаць інфармацыю аб файле з наладамі" + }, + { + "id": "app.file_info.get_for_post.app_error", + "translation": "Не атрымалася атрымаць інфармацыю аб файле да паведамлення." + }, + { + "id": "ent.ldap.syncronize.get_all.app_error", + "translation": "Не ўдалося атрымаць усіх карыстальнікаў з дапамогай AD/LDAP." + }, + { + "id": "ent.ldap.syncronize.delete_group_constained_memberships", + "translation": "памылка пры выдаленні ўдзелу ў камандзе ці канале" + }, + { + "id": "ent.ldap.save_user.username_exists.ldap_app_error", + "translation": "Уліковы запіс з такім імем карыстальніка ўжо існуе. Калі ласка, звяжыцеся з Адміністратарам." + }, + { + "id": "ent.ldap.save_user.email_exists.ldap_app_error", + "translation": "Гэты ўліковы запіс не выкарыстоўвае аўтэнтыфікацыю AD/LDAP. Калі ласка, увайдзіце, выкарыстоўваючы электронную пошту і пароль." + }, + { + "id": "ent.ldap.no.users.checkcertificate", + "translation": "Карыстальнікі LDAP не знойдзены, праверце карыстацкі фільтр і сертыфікаты." + }, + { + "id": "ent.ldap.do_login.x509.app_error", + "translation": "Памылка стварэння пары ключоў" + }, + { + "id": "ent.ldap.do_login.user_not_registered.app_error", + "translation": "Карыстальнік не зарэгістраваны на серверы AD/LDAP." + }, + { + "id": "ent.ldap.do_login.user_filtered.app_error", + "translation": "Ваш уліковы запіс AD/LDAP не мае дазволу на выкарыстанне гэтага сервера Mattermost. Калі ласка, звярніцеся да сістэмнага адміністратара, каб праверыць фільтр карыстальнікаў AD/LDAP." + }, + { + "id": "ent.ldap.do_login.unable_to_connect.app_error", + "translation": "Немагчыма падлучыцца да сервера AD/LDAP." + }, + { + "id": "ent.ldap.do_login.search_ldap_server.app_error", + "translation": "Не ўдалося знайсці сервер AD/LDAP." + }, + { + "id": "ent.ldap.do_login.matched_to_many_users.app_error", + "translation": "Дадзенае імя карыстальніка адпавядае некалькім карыстальнікам." + }, + { + "id": "ent.ldap.do_login.licence_disable.app_error", + "translation": "Функцыі AD/LDAP адключаныя ў выглядзе ліцэнзійных абмежаванняў. Калі ласка, звярніцеся да сістэмнага адміністратара для абнаўлення ліцэнзіі." + }, + { + "id": "ent.ldap.do_login.key.app_error", + "translation": "Памылка пры загрузцы файла ключа TLS для LDAP." + }, + { + "id": "ent.ldap.do_login.certificate.app_error", + "translation": "Памылка пры загрузцы файла TLS-сертыфіката LDAP." + }, + { + "id": "ent.ldap.do_login.bind_admin_user.app_error", + "translation": "Немагчыма зрабіць прывязку да сервера AD/LDAP. Праверце імя карыстальніка BindUsername і пароль BindPassword." + }, + { + "id": "ent.ldap.disabled.app_error", + "translation": "AD/LDAP адключаны ці ліцэнзія не падтрымлівае AD/LDAP." + }, + { + "id": "ent.ldap.create_fail", + "translation": "Немагчыма стварыць карыстальніка LDAP." + }, + { + "id": "ent.ldap.app_error", + "translation": "Інтэрфейс LDAP быў пусты." + }, + { + "id": "ent.jobs.start_synchronize_job.timeout", + "translation": "Дасягнуты тайм-аўт задання сінхранізацыі AD/LDAP." + }, + { + "id": "ent.id_loaded.license_disable.app_error", + "translation": "Ваша ліцэнзія не падтрымлівае push-паведамлення з ідэнтыфікатарам." + }, + { + "id": "ent.get_users_in_channel_during", + "translation": "Не ўдалося атрымаць карыстальнікаў у канале на працягу названага перыяду часу." + }, + { + "id": "ent.elasticsearch.test_config.reenter_password", + "translation": "URL або імя карыстальніка сервера Elasticsearch былі змененыя. Калі ласка, увядзіце Elasticsearch пароль яшчэ раз, каб праверыць злучэнне." + }, + { + "id": "ent.elasticsearch.test_config.license.error", + "translation": "Ваша версія сервера Mattermost не падтрымлівае Elasticsearch." + }, + { + "id": "ent.elasticsearch.stop.already_stopped.app_error", + "translation": "Elasticsearch ужо спынены." + }, + { + "id": "ent.elasticsearch.start.start_bulk_processor_failed.app_error", + "translation": "Не ўдалося запусціць масавы апрацоўшчык Elasticsearch." + }, + { + "id": "ent.elasticsearch.start.parse_server_version.app_error", + "translation": "Не ўдалося прааналізаваць версію сервера Elasticsearch." + }, + { + "id": "ent.elasticsearch.start.get_server_version.app_error", + "translation": "Не ўдалося атрымаць версію сервера Elasticsearch." + }, + { + "id": "ent.elasticsearch.start.create_bulk_processor_failed.app_error", + "translation": "Не ўдалося стварыць масавы апрацоўшчык Elasticsearch." + }, + { + "id": "ent.elasticsearch.start.already_started.app_error", + "translation": "Elasticsearch ужо запушчаны." + }, + { + "id": "ent.elasticsearch.search_users.unmarshall_user_failed", + "translation": "Не ўдалося дэкадаваць вынікі пошуку" + }, + { + "id": "ent.elasticsearch.search_users.search_failed", + "translation": "Пошук не ўдалося завяршыць" + }, + { + "id": "ent.elasticsearch.search_posts.unmarshall_post_failed", + "translation": "Не атрымалася дэкадаваць вынікі пошуку" + }, + { + "id": "ent.elasticsearch.search_posts.search_failed", + "translation": "Пошук не ўдалося завяршыць" + }, + { + "id": "ent.elasticsearch.search_posts.parse_matches_failed", + "translation": "Не ўдалося прааналізаваць супадзенні вынікаў пошуку" + }, + { + "id": "ent.elasticsearch.search_posts.disabled", + "translation": "Пошук Elasticsearch адключаны на гэтым сэрвэры" + }, + { + "id": "ent.elasticsearch.search_files.unmarshall_file_failed", + "translation": "Не атрымалася дэкадаваць вынікі пошуку" + }, + { + "id": "ent.elasticsearch.search_files.search_failed", + "translation": "Пошук не ўдалося завяршыць" + }, + { + "id": "ent.elasticsearch.search_files.disabled", + "translation": "На гэтым серверы адключаная функцыя пошуку файлаў Elasticsearch" + }, + { + "id": "ent.elasticsearch.search_channels.unmarshall_channel_failed", + "translation": "Не атрымалася дэкадаваць вынікі пошуку" + }, + { + "id": "ent.elasticsearch.search_channels.search_failed", + "translation": "Пошук не ўдалося завяршыць" + }, + { + "id": "ent.elasticsearch.search_channels.disabled", + "translation": "Пошук Elasticsearch адключаны на гэтым сэрвэры" + }, + { + "id": "ent.elasticsearch.refresh_indexes.refresh_failed", + "translation": "Не ўдалося абнавіць індэксы Elasticsearch" + }, + { + "id": "ent.elasticsearch.purge_indexes.delete_failed", + "translation": "Не атрымалася выдаліць азначнік Elasticsearch" + }, + { + "id": "ent.elasticsearch.post.get_posts_batch_for_indexing.error", + "translation": "Немагчыма атрымаць пакет паведамленняў для індэксацыі." + }, + { + "id": "ent.elasticsearch.post.get_files_batch_for_indexing.error", + "translation": "Не ўдалося атрымаць пакет файлаў для індэксацыі." + }, + { + "id": "ent.elasticsearch.not_started.error", + "translation": "Elasticsearch не запушчаны" + }, + { + "id": "ent.elasticsearch.indexer.index_batch.nothing_left_to_index.error", + "translation": "Спроба праіндэксаваць новы пакет, калі ўсе элементы былі завершаны" + }, + { + "id": "ent.elasticsearch.indexer.do_job.parse_start_time.error", + "translation": "Работніку індэксавання Elasticsearch не ўдалося прааналізаваць час пачатку" + }, + { + "id": "ent.elasticsearch.indexer.do_job.parse_end_time.error", + "translation": "Работніку індэксавання Elasticsearch не ўдалося прааналізаваць час заканчэння" + }, + { + "id": "ent.elasticsearch.indexer.do_job.get_oldest_entity.error", + "translation": "Самая старая сутнасць (карыстальнік, канал або пасада) не можа быць вынятая з базы дадзеных" + }, + { + "id": "ent.elasticsearch.index_user.error", + "translation": "Не ўдалося праіндэксаваць карыстальніка" + }, + { + "id": "ent.elasticsearch.index_post.error", + "translation": "Не ўдалося праіндэксаваць запіс" + }, + { + "id": "ent.elasticsearch.index_file.error", + "translation": "Не атрымалася праіндэксаваць файл" + }, + { + "id": "ent.elasticsearch.index_channels_batch.error", + "translation": "Немагчыма атрымаць пакет каналаў для індэксацыі." + }, + { + "id": "ent.elasticsearch.index_channel.error", + "translation": "Не ўдалося праіндэксаваць канал" + }, + { + "id": "ent.elasticsearch.generic.disabled", + "translation": "Пошук Elasticsearch не ўключаны на гэтым серверы" + }, + { + "id": "ent.elasticsearch.delete_user_posts.error", + "translation": "Не атрымалася выдаліць паведамленні карыстальніка" + }, + { + "id": "ent.elasticsearch.delete_user_files.error", + "translation": "Не атрымалася выдаліць файлы карыстальніка" + }, + { + "id": "ent.elasticsearch.delete_user.error", + "translation": "Не атрымалася выдаліць карыстальніка" + }, + { + "id": "ent.elasticsearch.delete_post_files.error", + "translation": "Не атрымалася выдаліць файлы паведамленняў" + }, + { + "id": "ent.elasticsearch.delete_post.error", + "translation": "Не атрымалася выдаліць плягін" + }, + { + "id": "ent.elasticsearch.delete_file.error", + "translation": "Не атрымалася выдаліць файл" + }, + { + "id": "ent.elasticsearch.delete_channel_posts.error", + "translation": "Не атрымалася выдаліць паведамленні канала" + }, + { + "id": "ent.elasticsearch.delete_channel.error", + "translation": "Не атрымалася выдаліць канал" + }, + { + "id": "ent.elasticsearch.data_retention_delete_indexes.get_indexes.error", + "translation": "Не ўдалося атрымаць індэксы Elasticsearch" + }, + { + "id": "ent.elasticsearch.data_retention_delete_indexes.delete_index.error", + "translation": "Не атрымалася выдаліць азначнік Elasticsearch" + }, + { + "id": "ent.elasticsearch.create_template_users_if_not_exists.template_create_failed", + "translation": "Не ўдалося стварыць шаблон Elasticsearch для карыстальнікаў" + }, + { + "id": "ent.elasticsearch.create_template_posts_if_not_exists.template_create_failed", + "translation": "Не атрымалася стварыць шаблон Elasticsearch для паведамленняў" + }, + { + "id": "ent.elasticsearch.create_template_file_info_if_not_exists.template_create_failed", + "translation": "Не ўдалося стварыць шаблон Elasticsearch для файлаў" + }, + { + "id": "ent.elasticsearch.create_template_channels_if_not_exists.template_create_failed", + "translation": "Не ўдалося стварыць шаблон Elasticsearch для каналаў" + }, + { + "id": "ent.elasticsearch.create_client.connect_failed", + "translation": "Памылка ўстаноўкі кліента Elasticsearch" + }, + { + "id": "ent.elasticsearch.aggregator_worker.index_job_failed.error", + "translation": "Збой у працы зборшчыка Elasticsearch з-за збою задання індэксавання" + }, + { + "id": "ent.elasticsearch.aggregator_worker.get_indexes.error", + "translation": "Работніку агрэгатара Elasticsearch не ўдалося атрымаць індэксы" + }, + { + "id": "ent.elasticsearch.aggregator_worker.delete_indexes.error", + "translation": "Работніку агрэгатара Elasticsearch не ўдалося выдаліць індэксы" + }, + { + "id": "ent.elasticsearch.aggregator_worker.create_index_job.error", + "translation": "Работніку агрэгатара Elasticsearch не ўдалося стварыць заданне індэксацыі" + }, + { + "id": "ent.data_retention.run_failed.error", + "translation": "Збой задання захавання дадзеных." + }, + { + "id": "ent.data_retention.policies.internal_error", + "translation": "Мы сутыкнуліся з памылкай пры выкананні запытанай аперацыі." + }, + { + "id": "ent.data_retention.generic.license.error", + "translation": "Ваша ліцэнзія не падтрымлівае SAML аўтэнтыфікацыю." + }, + { + "id": "ent.compliance.run_failed.error", + "translation": "Экспарт камплаенс-ліста не атрымаўся для задання '{{.JobName}}' у '{{.FilePath}}'" + }, + { + "id": "ent.compliance.run_export.template_watcher.appError", + "translation": "Немагчыма загрузіць шаблоны экспарту. Калі ласка, паспрабуйце яшчэ раз." + }, + { + "id": "ent.compliance.licence_disable.app_error", + "translation": "Функцыя камплаенсу недаступная пры бягучай ліцэнзіі. Калі ласка, звяжыцеся з сістэмным адміністратарам наконт паляпшэння вашай карпаратыўнай ліцэнзіі." + }, + { + "id": "ent.compliance.global_relay.write_file.appError", + "translation": "Немагчыма запісаць файл global relay." + }, + { + "id": "ent.compliance.global_relay.rewind_temporary_file.appError", + "translation": "Немагчыма перачытаць файл часовага экспарту Global Relay." + }, + { + "id": "ent.compliance.global_relay.open_temporary_file.appError", + "translation": "Немагчыма адкрыць часовы файл экспарту." + }, + { + "id": "ent.compliance.global_relay.attachments_removed.appError", + "translation": "Загружаны файл быў выдалены з экспарту Global Relay, паколькі ён быў занадта вялікім для адпраўкі." + }, + { + "id": "ent.compliance.csv.zip.creation.appError", + "translation": "Немагчыма стварыць файл экспарту ZIP." + }, + { + "id": "ent.compliance.csv.write_file.appError", + "translation": "Немагчыма запісаць csv-файл." + }, + { + "id": "ent.compliance.csv.warning.appError", + "translation": "Немагчыма стварыць файл апавяшчэння." + }, + { + "id": "ent.compliance.csv.post.export.appError", + "translation": "Немагчыма экспартаваць паведамленне." + }, + { + "id": "ent.compliance.csv.metadata.json.zipfile.appError", + "translation": "Немагчыма стварыць файл zip" + }, + { + "id": "ent.compliance.csv.metadata.json.marshalling.appError", + "translation": "Немагчыма пераўтварыць метададзеныя ў фармат json." + }, + { + "id": "ent.compliance.csv.metadata.export.appError", + "translation": "Немагчыма дадаць файл метададзеных у zip-файл." + }, + { + "id": "ent.compliance.csv.header.export.appError", + "translation": "Немагчыма дадаць загаловак у экспарт CSV." + }, + { + "id": "ent.compliance.csv.file.creation.appError", + "translation": "Немагчыма стварыць часовы файл экспарту CSV." + }, + { + "id": "ent.compliance.csv.attachment.export.appError", + "translation": "Немагчыма дадаць укладанне ў экспарт CSV." + }, + { + "id": "ent.compliance.csv.attachment.copy.appError", + "translation": "Немагчыма скапіяваць укладанне ў zip-файл." + }, + { + "id": "ent.compliance.bad_export_type.appError", + "translation": "Невядомы фармат вываду {{.ExportType}}" + }, + { + "id": "ent.cluster.timeout.error", + "translation": "Тайм-аўт чакання адказу кластара" + }, + { + "id": "ent.cluster.save_config.error", + "translation": "Сістэмная кансоль усталёўваецца толькі для чытання, калі ўключаная Высокая даступнасць, калі толькі ReadOnlyConfig не адключаны ў файле канфігурацыі." + }, + { + "id": "ent.cluster.json_encode.error", + "translation": "Адбылася памылка падчас маршалявання запыту JSON" + }, + { + "id": "ent.cluster.config_changed.info", + "translation": "Канфігурацыя кластара змянілася для id={{ .id }}. Кластар можа стаць нестабільным, і патрабуецца перазагрузка. Каб пераканацца, што кластар настроены правільна, вы павінны неадкладна выканаць паўторны перазапуск." + }, + { + "id": "ent.cluster.404.app_error", + "translation": "Не знойдзены канчатковы пункт API кластара." + }, + { + "id": "ent.actiance.export.write_file.appError", + "translation": "Не ўдалося запісаць экспартны файл." + }, + { + "id": "ent.actiance.export.marshalToXml.appError", + "translation": "Не ўдалося экспартаваць у XML." + }, + { + "id": "ent.account_migration.get_saml_users_failed", + "translation": "Не ўдалося атрымаць SAML карыстальнікаў." + }, + { + "id": "ent.account_migration.get_all_failed", + "translation": "Не ўдалося атрымаць карыстальнікаў." + }, + { + "id": "brand.save_brand_image.save_image.app_error", + "translation": "Немагчыма запісаць файл выявы ў сховішча файлаў. Калі ласка, праверце злучэнне і паспрабуйце зноў." + }, + { + "id": "brand.save_brand_image.open.app_error", + "translation": "Немагчыма загрузіць уласную выяву брэнда. Пераканайцеся, што памер выявы не перавышае 2 МБ, і паспрабуйце яшчэ раз." + }, + { + "id": "brand.save_brand_image.encode.app_error", + "translation": "Немагчыма пераўтварыць дадзеныя выявы ў фармат PNG. Калі ласка, паспрабуйце яшчэ раз." + }, + { + "id": "brand.save_brand_image.decode.app_error", + "translation": "Немагчыма дэкадаваць дадзеныя выявы." + }, + { + "id": "brand.save_brand_image.check_image_limits.app_error", + "translation": "Памылка праверкі абмежаванняў выявы. Дазвол занадта высокае." + }, + { + "id": "bleveengine.stop_user_index.error", + "translation": "Не ўдалося закрыць індэкс карыстальніка." + }, + { + "id": "bleveengine.stop_post_index.error", + "translation": "Не ўдалося закрыць індэкс паведамлення." + }, + { + "id": "bleveengine.stop_file_index.error", + "translation": "Не ўдалося закрыць індэкс файлаў." + }, + { + "id": "bleveengine.stop_channel_index.error", + "translation": "Не ўдалося закрыць індэкс канала." + }, + { + "id": "bleveengine.search_users_in_team.error", + "translation": "Пошук карыстальнікаў не ўдалося завяршыць." + }, + { + "id": "bleveengine.search_users_in_channel.uchan.error", + "translation": "Пошук карыстальнікаў не ўдалося завяршыць." + }, + { + "id": "bleveengine.search_users_in_channel.nuchan.error", + "translation": "Пошук карыстальнікаў не ўдалося завяршыць." + }, + { + "id": "bleveengine.search_posts.error", + "translation": "Пошук паведамленняў не ўдалося завяршыць." + }, + { + "id": "bleveengine.search_files.error", + "translation": "Пошук файлаў не завяршыўся." + }, + { + "id": "bleveengine.search_channels.error", + "translation": "Пошук каналаў не ўдалося завяршыць." + }, + { + "id": "bleveengine.purge_user_index.error", + "translation": "Не ўдалося ачысціць індэксы карыстальнікаў." + }, + { + "id": "bleveengine.purge_post_index.error", + "translation": "Не ўдалося ачысціць індэксы паведамленняў." + }, + { + "id": "bleveengine.purge_file_index.error", + "translation": "Не атрымалася ачысціць азначнікі файлаў." + }, + { + "id": "bleveengine.purge_channel_index.error", + "translation": "Не ўдалося ачысціць індэксы канала." + }, + { + "id": "bleveengine.indexer.index_batch.nothing_left_to_index.error", + "translation": "Спроба праіндэксаваць новы пакет, калі ўсе элементы ўжо былі завершаны." + }, + { + "id": "bleveengine.indexer.do_job.parse_start_time.error", + "translation": "Работніку індэксавання Bleve не ўдалося прааналізаваць час пачатку." + }, + { + "id": "bleveengine.indexer.do_job.parse_end_time.error", + "translation": "Работніку індэксавання Bleve не ўдалося прааналізаваць час заканчэння." + }, + { + "id": "bleveengine.indexer.do_job.get_oldest_entity.error", + "translation": "Самы стары аб'ект (карыстальнік, канал або паведамленне) не можа быць выняты з базы дадзеных." + }, + { + "id": "bleveengine.indexer.do_job.engine_inactive", + "translation": "Не атрымалася запусціць заданне індэксавання Bleve: рухавік неактыўны." + }, + { + "id": "bleveengine.indexer.do_job.bulk_index_users.batch_error", + "translation": "Не ўдалося праіндэксаваць пакет карыстальнікаў." + }, + { + "id": "bleveengine.indexer.do_job.bulk_index_posts.batch_error", + "translation": "Не ўдалося праіндэксаваць пакет паведамленняў." + }, + { + "id": "bleveengine.indexer.do_job.bulk_index_files.batch_error", + "translation": "Не атрымалася праіндэксаваць пакет файлаў." + }, + { + "id": "bleveengine.indexer.do_job.bulk_index_channels.batch_error", + "translation": "Не ўдалося праіндэксаваць пакет каналаў." + }, + { + "id": "bleveengine.index_user.error", + "translation": "Не ўдалося праіндэксаваць карыстальніка." + }, + { + "id": "bleveengine.index_post.error", + "translation": "Не ўдалося праіндэксаваць паведамленне." + }, + { + "id": "bleveengine.index_file.error", + "translation": "Не атрымалася праіндэксаваць файл." + }, + { + "id": "bleveengine.index_channel.error", + "translation": "Не ўдалося праіндэксаваць канал." + }, + { + "id": "bleveengine.delete_user_posts.error", + "translation": "Не атрымалася выдаліць паведамленні карыстальніка" + }, + { + "id": "bleveengine.delete_user_files.error", + "translation": "Не атрымалася выдаліць прыстасаваныя файлы." + }, + { + "id": "bleveengine.delete_user.error", + "translation": "Не атрымалася выдаліць карыстальніка." + }, + { + "id": "bleveengine.delete_post_files.error", + "translation": "Не атрымалася выдаліць файлы паведамленняў." + }, + { + "id": "bleveengine.delete_post.error", + "translation": "Не атрымалася выдаліць допіс." + }, + { + "id": "bleveengine.delete_files_batch.error", + "translation": "Не ўдалося выдаліць файлы." + }, + { + "id": "bleveengine.delete_file.error", + "translation": "Не ўдалося выдаліць файл." + }, + { + "id": "bleveengine.delete_channel_posts.error", + "translation": "Не атрымалася выдаліць паведамленні канала" + }, + { + "id": "bleveengine.delete_channel.error", + "translation": "Не атрымалася выдаліць канал." + }, + { + "id": "bleveengine.create_user_index.error", + "translation": "Памылка пры стварэнні азначніка карыстачоў bleve." + }, + { + "id": "bleveengine.create_post_index.error", + "translation": "Памылка пры стварэнні азначніка паведамленняў bleve." + }, + { + "id": "bleveengine.create_file_index.error", + "translation": "Памылка пры стварэнні азначніка bleve-файла." + }, + { + "id": "bleveengine.create_channel_index.error", + "translation": "Памылка пры стварэнні азначніка канала bleve." + }, + { + "id": "bleveengine.already_started.error", + "translation": "Bleve ужо запушчаны." + }, + { + "id": "app.webhooks.update_outgoing.app_error", + "translation": "Не ўдалося абнавіць вэбхук." + }, + { + "id": "app.file_info.get.app_error", + "translation": "Не атрымалася атрымаць інфармацыю аб файле." + }, + { + "id": "app.export.zip_create.error", + "translation": "Не ўдалося дадаць файл у zip-архіў пры экспарце." + }, + { + "id": "app.export.marshal.app_error", + "translation": "Немагчыма арганізаваць адказ." + }, + { + "id": "app.export.export_write_line.json_marshall.error", + "translation": "Адбылася памылка пры сартаванні JSON-дадзеных для экспарту." + }, + { + "id": "app.export.export_write_line.io_writer.error", + "translation": "Адбылася памылка пры запісе дадзеных экспарту." + }, + { + "id": "app.export.export_custom_emoji.copy_emoji_images.error", + "translation": "Немагчыма скапіяваць прыстасаваныя выявы смайлікаў" + }, + { + "id": "app.export.export_attachment.zip_create_header.error", + "translation": "Не ўдалося стварыць загаловак ZIP-архіва пры экспарце." + }, + { + "id": "app.export.export_attachment.mkdirall.error", + "translation": "Не ўдалося стварыць каталог пры экспарце." + }, + { + "id": "app.export.export_attachment.create_file.error", + "translation": "Не ўдалося стварыць файл пры экспарце." + }, + { + "id": "app.export.export_attachment.copy_file.error", + "translation": "Не ўдалося скапіяваць файл пры экспарце." + }, + { + "id": "app.emoji.get_list.internal_error", + "translation": "Не ўдалося атрымаць смайлік." + }, + { + "id": "app.emoji.get_by_name.no_result", + "translation": "Мы не змаглі знайсці смайлік." + }, + { + "id": "app.emoji.get_by_name.app_error", + "translation": "Не ўдалося атрымаць смайлік." + }, + { + "id": "app.emoji.get.no_result", + "translation": "Мы не змаглі знайсці смайлік." + }, + { + "id": "app.emoji.get.app_error", + "translation": "Не ўдалося атрымаць смайлік." + }, + { + "id": "app.emoji.delete.no_results", + "translation": "Не ўдалося знайсці смайлік для выдалення." + }, + { + "id": "app.emoji.delete.app_error", + "translation": "Не атрымалася выдаліць смайлік." + }, + { + "id": "app.emoji.create.internal_error", + "translation": "Не ўдалося захаваць эмоджы." + }, + { + "id": "app.email.setup_rate_limiter.app_error", + "translation": "Адбылася памылка ў абмежавальніку хуткасці." + }, + { + "id": "app.email.rate_limit_exceeded.app_error", + "translation": "Перавышаны мяжа колькасці рассылак па запрашэннях. Таймер будзе скінуты праз {{.ResetAfter}} секунд. Паўтарыце спробу праз {{.RetryAfter}} секунд." + }, + { + "id": "app.email.no_rate_limiter.app_error", + "translation": "Абмежавальнік хуткасці не настроены." + }, + { + "id": "app.create_basic_user.save_member.max_accounts.app_error", + "translation": "Немагчыма стварыць удзел у камандзе па змаўчанні, таму што ў гэтай камандзе не дазволены дадатковыя ўдзельнікі" + }, + { + "id": "app.create_basic_user.save_member.conflict.app_error", + "translation": "Немагчыма стварыць удзел у камандзе па змаўчанні, таму што ён ужо існуе" + }, + { + "id": "app.create_basic_user.save_member.app_error", + "translation": "Немагчыма стварыць удзел у камандзе па змаўчанні" + }, + { + "id": "app.compliance.save.saving.app_error", + "translation": "Узнікла памылка пры захаванні комплаенс-справаздачы." + }, + { + "id": "app.compliance.get.finding.app_error", + "translation": "Узнікла памылка з атрыманнем комплаенс-справаздач." + }, + { + "id": "app.command_webhook.try_use.internal_error", + "translation": "Немагчыма выкарыстоўваць вебхук." + }, + { + "id": "app.command_webhook.handle_command_webhook.parse", + "translation": "Немагчыма разабраць уваходныя дадзеныя." + }, + { + "id": "app.command_webhook.get.missing", + "translation": "Не ўдалося знайсці вэбхук." + }, + { + "id": "app.command_webhook.get.internal_error", + "translation": "Не ўдалося атрымаць вэбхук." + }, + { + "id": "app.command_webhook.create_command_webhook.internal_error", + "translation": "Немагчыма захаваць CommandWebhook." + }, + { + "id": "app.command_webhook.create_command_webhook.existing", + "translation": "Вы не можаце абнавіць існуючы CommandWebhook." + }, + { + "id": "app.command.updatecommand.internal_error", + "translation": "Не ўдалося абнавіць каманду." + }, + { + "id": "app.command.tryexecutecustomcommand.internal_error", + "translation": "Не атрымалася выканаць карыстацкую каманду." + }, + { + "id": "app.command.regencommandtoken.internal_error", + "translation": "Не ўдалося перастварыць токен каманды." + }, + { + "id": "app.command.movecommand.internal_error", + "translation": "Немагчыма перамясціць каманду." + }, + { + "id": "app.command.listteamcommands.internal_error", + "translation": "Немагчыма атрымаць спіс каманд Каманды." + }, + { + "id": "app.command.listautocompletecommands.internal_error", + "translation": "Немагчыма атрымаць спіс каманд аўтазапаўнення." + }, + { + "id": "app.command.listallcommands.internal_error", + "translation": "Немагчыма атрымаць спіс каманд." + }, + { + "id": "app.command.getcommand.internal_error", + "translation": "Немагчыма атрымаць каманду." + }, + { + "id": "app.command.deletecommand.internal_error", + "translation": "Немагчыма выдаліць каманду." + }, + { + "id": "app.command.createcommand.internal_error", + "translation": "Немагчыма захаваць каманду." + }, + { + "id": "app.channel_member_history.log_leave_event.internal_error", + "translation": "Не ўдалося запісаць гісторыю ўдзельнікаў канала. Не ўдалося абнавіць існуючы запіс аб далучэнні" + }, + { + "id": "app.channel_member_history.log_join_event.internal_error", + "translation": "Не ўдалося запісаць гісторыю ўдзельнікаў канала." + }, + { + "id": "app.channel.user_belongs_to_channels.app_error", + "translation": "Немагчыма вызначыць, ці належыць карыстач да спісу каналаў." + }, + { + "id": "app.channel.update_last_viewed_at_post.app_error", + "translation": "Немагчыма пазначыць канал як непрачытаны." + }, + { + "id": "app.channel.update_last_viewed_at.app_error", + "translation": "Не ўдалося ўстанавіць час апошняга прагляду." + }, + { + "id": "app.channel.update_channel.internal_error", + "translation": "Не ўдалося абнавіць канал." + }, + { + "id": "app.channel.update.bad_id", + "translation": "Не ўдалося абнавіць канал." + }, + { + "id": "app.channel.sidebar_categories.app_error", + "translation": "Не ўдалося ўставіць запіс у базу дадзеных." + }, + { + "id": "app.channel.search_group_channels.app_error", + "translation": "Немагчыма атрымаць групавыя каналы для дадзенага карыстальніка і зададзеных умоў." + }, + { + "id": "app.channel.search.app_error", + "translation": "Узнікла праблема пры пошуку канала." + }, + { + "id": "app.channel.save_member.exists.app_error", + "translation": "Удзельнік канала з такім ID ужо існуе." + }, + { + "id": "app.channel.restore.app_error", + "translation": "Не ўдалося аднавіць канал." + }, + { + "id": "app.channel.reset_all_channel_schemes.app_error", + "translation": "Мы не змаглі скінуць схемы канала." + }, + { + "id": "app.channel.remove_member.app_error", + "translation": "Немагчыма выдаліць удзельніка канала." + }, + { + "id": "app.bot.getbots.internal_error", + "translation": "Не ўдалося атрымаць ботаў." + }, + { + "id": "app.bot.getbot.internal_error", + "translation": "Не ўдалося атрымаць бота." + }, + { + "id": "app.bot.get_warn_metrics_bot.empty_admin_list.app_error", + "translation": "Спіс адміністратараў пусты." + }, + { + "id": "app.bot.get_system_bot.empty_admin_list.app_error", + "translation": "Спіс адміністратараў пусты." + }, + { + "id": "app.bot.get_disable_bot_sysadmin_message", + "translation": "{{if .disableBotsSetting}}{{if .printAllBots}}{{.UserName}} былі дэактываваны. Яны кіравалі наступнымі ўліковымі запісамі ботаў, якія зараз адключаныя.\n\n{{.BotNames}}{{else}}{{.UserName}} былі дэактываваны. Яны кіравалі {{.NumBots}} уліковымі запісамі ботаў, якія зараз адключаныя, уключаючы наступныя:\n\n{{.BotNames}}{{end}} Вы можаце стаць уладальнікам кожнага бота, улучыўшы яго ў раздзеле **Інтэграцыі > Уліковыя запісы ботаў** і стварыўшы новыя токены для бота.\n\nДля атрымання дадатковай інфармацыі гл. Нашу [дакументацыю](https://docs.mattermost.com/developer/bot-accounts.html#what-happens-when-a-user-who-owns-bot-accounts-is-disabled ).{{else}}{{if .printAllBots}}{{.UserName}} былі дэактываваны. Яны кіравалі наступнымі ўліковымі запісамі ботаў, якія ўсё яшчэ ўключаны.\n\n{{.BotNames}}\n{{else}}{{.UserName}} былі дэактываваны. Яны кіравалі {{.NumBots}} уліковымі запісамі ботаў, якія па-ранейшаму ўключаны, уключаючы наступныя:\n\n{{.BotNames}}{{end}} Мы настойліва раім вам стаць уладальнікам кожнага бота, паўторна ўключыўшы яго ў раздзеле **Інтэграцыя > Уліковыя запісы ботаў** і стварыўшы новыя токены для бота.\n\nДля атрымання дадатковай інфармацыі гл. Нашу [дакументацыю](https://docs.mattermost.com/developer/bot-accounts.html#what-happens-when-a-user-who-owns-bot-accounts-is-disabled ).\n\nКалі вы хочаце, каб уліковыя запісы ботаў аўтаматычна адключаліся пасля дэактывацыі карыстальніка, усталюйце для параметра «Адключыць уліковыя запісы ботаў пасля дэактывацыі карыстальніка» у **Сістэмная кансоль > Інтэграцыя > Уліковыя запісы ботаў** значэнне \"так\".{{end}}" + }, + { + "id": "app.bot.createbot.internal_error", + "translation": "Не ўдалося захаваць робата." + }, + { + "id": "app.audit.save.saving.app_error", + "translation": "Мы сутыкнуліся з памылкай пры захаванні аўдытаў." + }, + { + "id": "app.audit.permanent_delete_by_user.app_error", + "translation": "Мы сутыкнуліся з памылкай пры выдаленні аўдытаў." + }, + { + "id": "app.audit.get.limit.app_error", + "translation": "Перавышаны ліміт для падзелу па старонках." + }, + { + "id": "app.audit.get.finding.app_error", + "translation": "Мы знайшлі памылку пры пошуку аўдытаў." + }, + { + "id": "app.analytics.getanalytics.internal_error", + "translation": "Немагчыма атрымаць аналітыку." + }, + { + "id": "app.admin.test_site_url.failure", + "translation": "Гэта URL спасылка не працуе" + }, + { + "id": "app.admin.saml.invalid_response_from_idp.app_error", + "translation": "Не ўдалося прачытаць адказ, атрыманы ад пастаўшчыка пасведчанняў." + }, + { + "id": "app.admin.saml.failure_read_response_body_from_idp.app_error", + "translation": "Выяўлена памылка пры чытанні карыснай нагрузкі адказу, атрыманай ад пастаўшчыка пасведчанняў." + }, + { + "id": "app.admin.saml.failure_decode_metadata_xml_from_idp.app_error", + "translation": "Не атрымалася дэкадаваць інфармацыю метададзеных XML, атрыманую ад пастаўшчыка пасведчанняў." + }, + { + "id": "api.websocket_handler.server_busy.app_error", + "translation": "Сервер заняты, некрытычныя службы часова недаступныя." + }, + { + "id": "api.webhook.update_outgoing.intersect.app_error", + "translation": "Выходныя вэбхукі аднаго канала не могуць мець аднолькавыя словы-трыгеры / выкліканыя URL." + }, + { + "id": "api.webhook.team_mismatch.app_error", + "translation": "Не ўдалося абнавіць вэбхук у розных камандах." + }, + { + "id": "api.webhook.incoming.error", + "translation": "Не атрымалася дэкадаваць шматчастковую карысную нагрузку ўваходнага вэбхука." + }, + { + "id": "api.webhook.create_outgoing.triggers.app_error", + "translation": "Павінны быць зададзены trigger_words ці channel_id." + }, + { + "id": "api.webhook.create_outgoing.permissions.app_error", + "translation": "Неадпаведныя правы для стварэння выходнага вэбхука." + }, + { + "id": "api.webhook.create_outgoing.not_open.app_error", + "translation": "Выходныя вэбхукі могуць быць створаны толькі для грамадскіх каналаў." + }, + { + "id": "api.webhook.create_outgoing.intersect.app_error", + "translation": "Выходныя вэбхукі аднаго канала не могуць мець аднолькавыя словы-трыгеры / выкліканыя URL." + }, + { + "id": "api.web_socket_router.not_authenticated.app_error", + "translation": "Падлучэнне WebSocket не мінула праверку сапраўднасці. Калі ласка, увайдзіце ў сістэму і паспрабуйце яшчэ раз." + }, + { + "id": "api.web_socket_router.bad_seq.app_error", + "translation": "Недапушчальная паслядоўнасць паведамленняў праз websocket." + }, + { + "id": "api.web_socket.connect.upgrade.app_error", + "translation": "Не ўдалося абнавіць злучэнне з web-сокетам." + }, + { + "id": "api.user.verify_email.token_parse.error", + "translation": "Не ўдалося прааналізаваць дадзеныя токена з пацверджання па электроннай пошце" + }, + { + "id": "api.user.verify_email.link_expired.app_error", + "translation": "Тэрмін дзеяння спасылкі для пацверджання адраса электроннай пошты скончыўся." + }, + { + "id": "api.user.verify_email.broken_token.app_error", + "translation": "Дрэнны тып токена праверкі email." + }, + { + "id": "api.user.verify_email.bad_link.app_error", + "translation": "Дрэнная спасылка праверкі email." + }, + { + "id": "api.user.upload_profile_user.upload_profile.app_error", + "translation": "Немагчыма загрузіць выяву профіля." + }, + { + "id": "api.user.upload_profile_user.too_large.app_error", + "translation": "Немагчыма загрузіць выяву. Файл занадта вялікі." + }, + { + "id": "api.user.upload_profile_user.storage.app_error", + "translation": "Немагчыма загрузіць файл. Сховішча малюнкаў не наладжана." + }, + { + "id": "api.user.upload_profile_user.parse.app_error", + "translation": "Не ўдалося разабраць складовую форму." + }, + { + "id": "api.user.upload_profile_user.open.app_error", + "translation": "Не атрымалася адкрыць файл выявы." + }, + { + "id": "api.user.upload_profile_user.no_file.app_error", + "translation": "Няма файла 'image' у запыце." + }, + { + "id": "api.user.upload_profile_user.login_provider_attribute_set.app_error", + "translation": "Малюнак профілю павінна быць усталявана праз правайдэра аўтэнтыфікацыі карыстальніка." + }, + { + "id": "api.user.upload_profile_user.encode.app_error", + "translation": "Не атрымалася закадаваць выяву профіля." + }, + { + "id": "api.user.upload_profile_user.decode.app_error", + "translation": "Не атрымалася дэкадаваць выяву профіля." + }, + { + "id": "api.user.upload_profile_user.check_image_limits.app_error", + "translation": "Памылка праверкі абмежаванняў выявы. Дазвол занадта высокае." + }, + { + "id": "api.user.upload_profile_user.array.app_error", + "translation": "Пусты масіў 'image' у запыце." + }, + { + "id": "api.user.update_user_roles.license.app_error", + "translation": "Дадзеныя схемы карыстацкіх правоў не падтрымліваюцца бягучай ліцэнзіяй" + }, + { + "id": "api.user.update_user_auth.invalid_request", + "translation": "У запыце адсутнічае альбо параметр AuthData, альбо параметр AuthService." + }, + { + "id": "api.user.update_user.login_provider_attribute_set.app_error", + "translation": "Поле '{{.Field}}' павінна быць усталявана праз правайдэра аўтэнтыфікацыі карыстальніка." + }, + { + "id": "api.user.update_user.accepted_guest_domain.app_error", + "translation": "Указаны вамі адрас электроннай пошты не належыць прызнанаму дамену для гасцявых уліковых запісаў. Калі ласка, звяжыцеся з вашым адміністратарам ці зарэгіструйцеся з іншым адрасам электроннай пошты." + }, + { + "id": "api.user.update_user.accepted_domain.app_error", + "translation": "Указаны вамі адрас электроннай пошты не належыць прызнанаму дамену. Калі ласка, звяжыцеся з вашым адміністратарам ці зарэгіструйцеся з іншым адрасам электроннай пошты." + }, + { + "id": "api.user.update_password.valid_account.app_error", + "translation": "Не атрымалася абнавіць пароль, паколькі мы не змаглі знайсці сапраўдны ўліковы запіс." + }, + { + "id": "api.user.update_password.user_and_hashed.app_error", + "translation": "Толькі сістэмныя адміністратары могуць усталёўваць ужо хэшаваныя паролі." + }, + { + "id": "api.user.update_password.oauth.app_error", + "translation": "Не атрымалася абнавіць пароль, паколькі карыстач увайшоў праз службу OAuth." + }, + { + "id": "api.user.update_password.menu", + "translation": "выкарыстоўваючы меню настроек" + }, + { + "id": "api.user.update_password.incorrect.app_error", + "translation": "Уведзены вамі \"Бягучы пароль\" няверны. Калі ласка, праверце выключаны Caps Lock і паспрабуйце зноў." + }, + { + "id": "api.user.update_password.context.app_error", + "translation": "Не атрымалася абнавіць пароль, паколькі кантэкст user_id не адпавядаў рэквізітам user_id." + }, + { + "id": "api.user.update_oauth_user_attrs.get_user.app_error", + "translation": "Немагчыма атрымаць карыстальніка з {{.Service}} аб'екта карыстальніка." + }, + { + "id": "api.user.update_active.permissions.app_error", + "translation": "У вас няма адпаведных правоў." + }, + { + "id": "api.user.update_active.not_enable.app_error", + "translation": "Вы не можаце дэактываваць сябе самастойна, таму што гэтая функцыя не ўключана. Калі ласка, звяжыцеся са сваім сістэмным адміністратарам." + }, + { + "id": "api.user.update_active.cannot_enable_guest_when_guest_feature_is_disabled.app_error", + "translation": "Вы не можаце актываваць гасцявы ўліковы запіс, таму што функцыя гасцявога доступу не ўключана." + }, + { + "id": "api.user.send_verify_email_and_forget.failed.error", + "translation": "Не ўдалося адправіць ліст з пацверджаннем" + }, + { + "id": "api.user.send_sign_in_change_email_and_forget.error", + "translation": "Не атрымалася адправіць ліст аб абнаўленні пароля" + }, + { + "id": "api.user.send_password_reset.sso.app_error", + "translation": "Немагчыма скінуць пароль для ўліковых запісаў SSO." + }, + { + "id": "api.user.send_password_reset.send.app_error", + "translation": "Не ўдалося адправіць ліст аб паспяховым скідзе пароля." + }, + { + "id": "api.user.send_email_change_verify_email_and_forget.error", + "translation": "Не ўдалося адправіць ліст з пацвярджэннем змены адраса электроннай пошты" + }, + { + "id": "api.user.send_deactivate_email_and_forget.failed.error", + "translation": "Не атрымалася адправіць паведамленне на электронную пошту аб паспяховай дэактывацыі акаўнта" + }, + { + "id": "api.user.send_cloud_welcome_email.error", + "translation": "Не ўдалося адправіць прывітальны ліст па электроннай пошце" + }, + { + "id": "api.user.saml.not_available.app_error", + "translation": "SAML 2.0 не наладжаны ці не падтрымліваецца на гэтым серверы." + }, + { + "id": "api.user.reset_password.token_parse.error", + "translation": "Немагчыма прааналізаваць токен скіду пароля" + }, + { + "id": "api.user.reset_password.sso.app_error", + "translation": "Немагчыма скінуць пароль для ўліковых запісаў SSO." + }, + { + "id": "api.user.reset_password.method", + "translation": "выкарыстоўваючы спасылку для скіду пароля" + }, + { + "id": "api.user.reset_password.link_expired.app_error", + "translation": "Спасылка для скіду пароля пратэрмінаваная." + }, + { + "id": "api.user.reset_password.invalid_link.app_error", + "translation": "Спасылка скіду пароля не ўяўляецца сапраўднай." + }, + { + "id": "api.user.reset_password.broken_token.app_error", + "translation": "Токен скіду пароля не ўяўляецца сапраўдным." + }, + { + "id": "api.user.promote_guest_to_user.no_guest.app_error", + "translation": "Немагчыма пераўтварыць госця ў звычайнага карыстальніка, таму што ён не госць." + }, + { + "id": "api.user.patch_user.login_provider_attribute_set.app_error", + "translation": "Поле '{{.Field}}' павінна быць усталявана праз правайдэра аўтэнтыфікацыі карыстальніка." + }, + { + "id": "api.user.oauth_to_email.not_available.app_error", + "translation": "Перадача аўтэнтыфікацыі не наладжана ці недаступная на гэтым серверы." + }, + { + "id": "api.user.oauth_to_email.context.app_error", + "translation": "Не ўдалося абнавіць пароль, паколькі кантэкст user_id не адпавядае дадзенаму ідэнтыфікатару карыстальніка." + }, + { + "id": "api.user.login_ldap.not_available.app_error", + "translation": "AD/LDAP недаступны на гэтым сэрвэры." + }, + { + "id": "api.user.login_cws.license.error", + "translation": "Уваход у CWS забаронены." + }, + { + "id": "api.user.login_by_oauth.parse.app_error", + "translation": "Немагчыма распарсіць дадзеныя аўтарызацыі з {{.Service}} аб'екта карыстальніка." + }, + { + "id": "api.user.login_by_oauth.not_available.app_error", + "translation": "{{.Service}} SSO праз OAuth 2.0 не падтрымліваецца на гэтым серверы." + }, + { + "id": "api.user.login_by_oauth.bot_login_forbidden.app_error", + "translation": "Лагін бота забаронены." + }, + { + "id": "api.user.login_by_cws.invalid_token.app_error", + "translation": "Токен CWS несапраўдны" + }, + { + "id": "api.user.login.use_auth_service.app_error", + "translation": "Калі ласка, увайдзіце з дапамогай {{.AuthService}}." + }, + { + "id": "api.user.login.not_verified.app_error", + "translation": "Уваход не атрымаўся, паколькі адрас электроннай пошты не быў пацверджаны." + }, + { + "id": "api.user.login.invalid_credentials_username", + "translation": "Увядзіце сапраўднае імя карыстальніка і/або пароль." + }, + { + "id": "api.user.login.invalid_credentials_sso", + "translation": "Увядзіце сапраўдны адрас электроннай пошты ці імя карыстальніка і/або пароль, або ўвайдзіце ў сістэму, выкарыстоўваючы іншы метад." + }, + { + "id": "api.user.login.invalid_credentials_email_username", + "translation": "Увядзіце сапраўдны адрас электроннай пошты ці імя карыстальніка і/або пароль." + }, + { + "id": "api.user.login.invalid_credentials_email", + "translation": "Увядзіце сапраўдны адрас электроннай пошты і/або пароль" + }, + { + "id": "api.user.login.inactive.app_error", + "translation": "Уваход не атрымаўся, паколькі ваш уліковы запіс адключаны. Калі ласка, звяжыцеся з адміністратарам." + }, + { + "id": "api.user.login.guest_accounts.license.error", + "translation": "Ваша ліцэнзія не падтрымлівае гасцявыя ўліковыя запісы" + }, + { + "id": "api.user.login.guest_accounts.disabled.error", + "translation": "Гасцявыя ўліковыя запісы адключаныя" + }, + { + "id": "api.user.login.client_side_cert.license.app_error", + "translation": "Спроба выкарыстання эксперыментальнай магчымасці ClientSideCert без сапраўднай карпаратыўнай ліцэнзіі." + }, + { + "id": "api.user.login.client_side_cert.certificate.app_error", + "translation": "Спроба ўвайсці з выкарыстаннем эксперыментальнай магчымасці ClientSideCert без прадастаўлення сапраўднага сертыфіката." + }, + { + "id": "api.user.login.bot_login_forbidden.app_error", + "translation": "Лагін бота забаронены." + }, + { + "id": "api.user.login.blank_pwd.app_error", + "translation": "Поле з паролем не павінна быць пустым" + }, + { + "id": "api.user.ldap_to_email.not_ldap_account.app_error", + "translation": "Уліковы запіс карыстальніка не выкарыстоўвае AD/LDAP." + }, + { + "id": "api.user.ldap_to_email.not_available.app_error", + "translation": "AD/LDAP недаступны на гэтым сэрвэры." + }, + { + "id": "api.user.invalidate_verify_email_tokens_parse.error", + "translation": "Немагчыма прааналізаваць токен пры ануляванні токенаў пацверджання электроннай пошты" + }, + { + "id": "api.user.invalidate_verify_email_tokens_delete.error", + "translation": "Немагчыма выдаліць токен пры ануляванні токенаў пацверджання электроннай пошты" + }, + { + "id": "api.user.invalidate_verify_email_tokens.error", + "translation": "Немагчыма атрымаць токены па тыпе пры ануляванні токенаў пацверджання электроннай пошты" + }, + { + "id": "api.user.get_user_by_email.permissions.app_error", + "translation": "Немагчыма вызначыць карыстальніка па электроннай пошце." + }, + { + "id": "api.user.get_uploads_for_user.forbidden.app_error", + "translation": "Не ўдалося атрымаць загрузкі." + }, + { + "id": "api.user.get_authorization_code.endpoint.app_error", + "translation": "Памылка атрымання endpoint з Discovery Document." + }, + { + "id": "api.user.email_to_oauth.not_available.app_error", + "translation": "Перадача аўтэнтыфікацыі не наладжана ці недаступная на гэтым серверы." + }, + { + "id": "api.user.email_to_ldap.not_available.app_error", + "translation": "AD/LDAP недаступны на гэтым сэрвэры." + }, + { + "id": "api.user.demote_user_to_guest.already_guest.app_error", + "translation": "Немагчыма пераўтварыць карыстальніка ў госця, паколькі ён ужо з'яўляецца госцем." + }, + { + "id": "api.user.delete_user.not_enabled.app_error", + "translation": "Функцыя беззваротнага выдалення карыстальніка не ўключана. Калі ласка, звярніцеся да сістэмнага адміністратара." + }, + { + "id": "api.user.delete_team.not_enabled.app_error", + "translation": "Функцыя выдалення каманды назаўжды не ўключана. Калі ласка, звярніцеся да сістэмнага адміністратара." + }, + { + "id": "api.user.delete_channel.not_enabled.app_error", + "translation": "Функцыя выдалення каманды назаўжды недаступная. Калі ласка, звярніцеся да сістэмнага адміністратара." + }, + { + "id": "api.user.create_user.signup_link_invalid.app_error", + "translation": "Спасылка для рэгістрацыі, здаецца, няправільная." + }, + { + "id": "api.user.create_user.signup_link_expired.app_error", + "translation": "Спасылка для рэгістрацыі састарэлая." + }, + { + "id": "api.user.create_user.signup_email_disabled.app_error", + "translation": "Рэгістрацыя з электроннай поштай адключана." + }, + { + "id": "api.user.create_user.no_open_server", + "translation": "Гэты сервер не дазваляе адчыненую рэгістрацыю. Калі ласка, паразмаўляйце з Адміністратарам для атрымання запрашэння." + }, + { + "id": "api.user.create_user.invalid_invitation_type.app_error", + "translation": "Немагчыма стварыць карыстальніка, несапраўднае запрашэнне." + }, + { + "id": "api.user.create_user.guest_accounts.license.app_error", + "translation": "Ваша ліцэнзія не падтрымлівае гасцявыя ўліковыя запісы." + }, + { + "id": "api.user.create_user.guest_accounts.disabled.app_error", + "translation": "Гасцявыя ўліковыя запісы адключаныя." + }, + { + "id": "api.user.create_user.disabled.app_error", + "translation": "Стварэнне акаўнтаў адключана." + }, + { + "id": "api.user.create_user.accepted_domain.app_error", + "translation": "Указаны вамі адрас электроннай пошты не належыць прызнанаму дамену. Калі ласка, звяжыцеся з вашым адміністратарам ці зарэгіструйцеся з іншым адрасам электроннай пошты." + }, + { + "id": "api.user.create_profile_image.initial.app_error", + "translation": "Немагчыма дадаць ініцыялы карыстальніка да выявы профіля па змаўчанні." + }, + { + "id": "api.user.create_profile_image.encode.app_error", + "translation": "Немагчыма закадаваць выяву профілю па змаўчанні." + }, + { + "id": "api.user.create_profile_image.default_font.app_error", + "translation": "Немагчыма стварыць шрыфт выявы профіля па змаўчанні." + }, + { + "id": "api.user.create_password_token.error", + "translation": "Немагчыма стварыць токен аднаўлення пароля" + }, + { + "id": "api.user.create_oauth_user.create.app_error", + "translation": "Немагчыма стварыць карыстальніка з {{.Service}} аб'екта карыстальніка." + }, + { + "id": "api.user.create_oauth_user.already_attached.app_error", + "translation": "Ужо існуе ўліковы запіс злучаны з гэтым адрасам электроннай пошты, якая выкарыстоўвае метад уваходу выдатны ад {{.Service}}. Калі ласка, увайдзіце, выкарыстоўваючы {{.Auth}}." + }, + { + "id": "api.user.create_email_token.error", + "translation": "Не ўдалося стварыць дадзеныя токена для праверкі электроннай пошты" + }, + { + "id": "api.user.complete_switch_with_oauth.parse.app_error", + "translation": "Немагчыма распарсіць дадзеныя аўтарызацыі з {{.Service}} аб'екта карыстальніка." + }, + { + "id": "api.user.check_user_password.invalid.app_error", + "translation": "Памылка ўваходу, няправільна ўведзены пароль." + }, + { + "id": "api.user.check_user_login_attempts.too_many.app_error", + "translation": "Ваш уліковы запіс быў заблакаваны з-за занадта вялікай колькасці няўдалых спроб уводу пароля. Калі ласка, скіньце свой пароль." + }, + { + "id": "api.user.autocomplete_users.missing_team_id.app_error", + "translation": "Параметр ідэнтыфікатара каманды неабходны для аўтазапаўнення па канале." + }, + { + "id": "api.user.authorize_oauth_user.unsupported.app_error", + "translation": "{{.Service}} SSO праз OAuth 2.0 не падтрымліваецца на гэтым серверы." + }, + { + "id": "api.user.authorize_oauth_user.service.app_error", + "translation": "Запыт токена для {{.Service}} не атрымаўся." + }, + { + "id": "api.user.authorize_oauth_user.response.app_error", + "translation": "Атрыманы няправільны адказ ад пастаўшчыка паслуг OAuth." + }, + { + "id": "api.user.authorize_oauth_user.bad_response.app_error", + "translation": "Дрэнны адказ ад запыту токена." + }, + { + "id": "api.user.add_direct_channels_and_forget.failed.error", + "translation": "Не атрымалася ўжыць наладкі прыватнага канала для карыстальніка: user_id={{.UserId}}, team_id={{.TeamId}}, err={{.Error}}" + }, + { + "id": "api.user.activate_mfa.email_and_ldap_only.app_error", + "translation": "МФА недаступная для дадзенага тыпу акаўнта." + }, + { + "id": "api.upload.upload_data.multipart_error", + "translation": "Не ўдалося апрацаваць састаўныя даныя." + }, + { + "id": "api.upload.upload_data.invalid_content_type", + "translation": "Недапушчальны Content-Type для складовай загрузкі." + }, + { + "id": "api.upload.get_upload.forbidden.app_error", + "translation": "Не ўдалося атрымаць загрузкі." + }, + { + "id": "api.upgrade_to_enterprise_status.signature.app_error", + "translation": "Mattermost не ўдалося абнавіць да Карпаратыўнай рэдакцыі. Не атрымалася праверыць лічбавы подпіс загружанага двайковага файла." + }, + { + "id": "api.upgrade_to_enterprise_status.app_error", + "translation": "Mattermost не ўдалося абнавіць да Карпаратыўнай рэдакцыі." + }, + { + "id": "api.upgrade_to_enterprise.system_not_supported.app_error", + "translation": "Mattermost не ўдалося абнавіць да Карпаратыўнай рэдакцыі. Гэта функцыя будзе працаваць толькі ў сістэмах Linux з архітэктурай x86-64." + }, + { + "id": "api.upgrade_to_enterprise.invalid-user.app_error", + "translation": "Mattermost не ўдалося абнавіць да Карпаратыўнай рэдакцыі. Сістэмны карыстач Mattermost {{.MattermostUsername}} не мае правоў на запіс у неабходны двайковы файл. Сістэмны адміністратар можа абнавіць правы доступу да файлаў, выканаўшы наступную каманду на серверы, дзе ўсталяваны Mattermost:\n\n```\nchown {{.MattermostUsername}} \"{{.Path}}\"\n```\n\nПасля змены правоў доступу да файла паспрабуйце зноў абнавіць Mattermost. Пасля абнаўлення і перазапуску не забудзьцеся аднавіць зыходныя дазволы для двайковага файла:\n\n```\nchown {{.FileUsername}} \"{{.Path}}\"\n```" + }, + { + "id": "api.upgrade_to_enterprise.invalid-user-and-permission.app_error", + "translation": "Mattermost не ўдалося абнавіць да Карпаратыўнай рэдакцыі. Сістэмны карыстач Mattermost {{.MattermostUsername}} не мае правоў на запіс у неабходны двайковы файл. Сістэмны адміністратар можа абнавіць правы доступу да файлаў, выканаўшы наступную каманду на серверы, дзе ўсталяваны Mattermost:\n\n```\nchown {{.MattermostUsername}} \"{{.Path}}\"\nchmod +w \"{{.Path}}\"\n```\n\nПасля змены правоў доступу да файла паспрабуйце зноў абнавіць Mattermost. Пасля абнаўлення і перазапуску не забудзьцеся аднавіць зыходныя дазволы для двайковага файла:\n\n```\nchown {{.FileUsername}} \"{{.Path}}\"\nchmod -w \"{{.Path}}\"\n```" + }, + { + "id": "api.upgrade_to_enterprise.invalid-permission.app_error", + "translation": "Mattermost не ўдалося абнавіць да Карпаратыўнай рэдакцыі. Сістэмны карыстач Mattermost {{.MattermostUsername}} не мае правоў на запіс у неабходны двайковы файл. Сістэмны адміністратар можа абнавіць правы доступу да файлаў, выканаўшы наступную каманду на серверы, дзе ўсталяваны Mattermost:\n\n```\nchmod +w \"{{.Path}}\"\n```\n\nПасля змены правоў доступу да файла паспрабуйце зноў абнавіць Mattermost. Пасля абнаўлення і перазапуску не забудзьцеся аднавіць зыходныя дазволы для двайковага файла:\n\n```\nchmod -w \"{{.Path}}\"\n```" + }, + { + "id": "api.upgrade_to_enterprise.generic_error.app_error", + "translation": "Mattermost не ўдалося абнавіць да Карпаратыўнай рэдакцыі." + }, + { + "id": "api.upgrade_to_enterprise.app_error", + "translation": "Абнаўленне да Mattermost Карпаратыўная рэдакцыя ўжо выконваецца." + }, + { + "id": "api.upgrade_to_enterprise.already-enterprise.app_error", + "translation": "Вы не можаце выканаць абнаўленне, таму што вы ўжо карыстаецеся Mattermost Карпаратыўная рэдакцыя." + }, + { + "id": "api.upgrade_to_enterprise.already-done.app_error", + "translation": "Вы ўжо перайшлі на Mattermost Карпаратыўная рэдакцыя. Калі ласка, перазапусціце сервер, каб завяршыць абнаўленне." + }, + { + "id": "api.unable_to_read_file_from_backend", + "translation": "Памылка чытання файла з бэкэнда" + }, + { + "id": "api.unable_to_create_zip_file", + "translation": "Памылка стварэння zip-файла." + }, + { + "id": "api.templates.welcome_subject", + "translation": "[{{ .SiteName }}] Вы далучыліся да {{ .ServerURL }}" + }, + { + "id": "api.templates.welcome_body.title", + "translation": "Сардэчна запрашаем у каманду" + }, + { + "id": "api.templates.welcome_body.subTitle2", + "translation": "Пстрыкніце ніжэй, каб пацвердзіць ваш email-адрас." + }, + { + "id": "api.templates.welcome_body.info1", + "translation": "Калі гэта былі не вы, вы можаце смела ігнараваць гэтае паведамленне." + }, + { + "id": "api.templates.welcome_body.info", + "translation": "Гэты email-адрас быў выкарыстаны для стварэння ўліковага запісу Mattermost." + }, + { + "id": "api.templates.welcome_body.app_download_title", + "translation": "Спампаваць настольныя і мабільныя прыкладанні" + }, + { + "id": "api.templates.welcome_body.app_download_info", + "translation": "Для зручнасці, Вы можаце спампаваць прыкладанні для PC, Mac, iOS and Android." + }, + { + "id": "api.templates.warn_metric_ack.subject", + "translation": "Запыт Mattermost Звяжыцеся з намі" + }, + { + "id": "api.templates.warn_metric_ack.footer", + "translation": "Калі ў вас ёсць якія-небудзь дадатковыя пытанні, калі ласка, звяжыцеся з support@mattermost.com" + }, + { + "id": "api.templates.verify_subject", + "translation": "[{{ .SiteName }}] Пацвярджэнне email" + }, + { + "id": "api.templates.verify_body.title", + "translation": "Пацвердзіць email-адрас" + }, + { + "id": "api.templates.verify_body.subTitle2", + "translation": "Пстрыкніце ніжэй, каб пацвердзіць ваш email-адрас." + }, + { + "id": "app.plugin.signature_decode.app_error", + "translation": "Не атрымалася дэкадаваць сігнатуру base64." + }, + { + "id": "app.plugin.restart.app_error", + "translation": "Не ўдалося перазапусціць убудову падчас абнаўлення." + }, + { + "id": "app.plugin.remove_bundle.app_error", + "translation": "Не атрымалася выдаліць плягін з файлавага сховішча." + }, + { + "id": "app.plugin.remove.app_error", + "translation": "Немагчыма выдаліць убудову." + }, + { + "id": "app.plugin.not_installed.app_error", + "translation": "Убудова не ўсталявана." + }, + { + "id": "app.plugin.mvdir.app_error", + "translation": "Немагчыма перамясціць убудову з часовага каталога ў канчатковы пункт прызначэння. Іншы плягін можа выкарыстоўваць тое ж імя каталога." + }, + { + "id": "app.plugin.modify_saml.app_error", + "translation": "Не ўдалося мадыфікаваць файлы saml." + }, + { + "id": "app.plugin.marshal.app_error", + "translation": "Не атрымалася вырабіць маршаляванне плагінаў з крамы плагінаў." + }, + { + "id": "app.plugin.marketplace_plugins.signature_not_found.app_error", + "translation": "Не атрымалася знайсці сігнатуру запытанага плагіна з крамы плагінаў." + }, + { + "id": "app.plugin.marketplace_plugins.not_found.app_error", + "translation": "Не атрымалася знайсці запытаны плагін з крамы плагінаў." + }, + { + "id": "app.plugin.marketplace_plugin_request.app_error", + "translation": "Не атрымалася дэкадаваць запыт ад крамы плагінаў." + }, + { + "id": "app.plugin.marketplace_disabled.app_error", + "translation": "Крама плагінаў была адключаная. Для падрабязнай інфармацыі гл. логі." + }, + { + "id": "app.plugin.marketplace_client.failed_to_fetch", + "translation": "Не атрымалася атрымаць убудовы з сервера крамы убудоў." + }, + { + "id": "app.plugin.marketplace_client.app_error", + "translation": "Не атрымалася стварыць кліента для крамы плагінаў." + }, + { + "id": "app.plugin.manifest.app_error", + "translation": "Немагчыма знайсці маніфест для вынятага плагіна." + }, + { + "id": "app.plugin.invalid_version.app_error", + "translation": "Немагчыма разабраць версію плагіна." + }, + { + "id": "app.plugin.invalid_id.app_error", + "translation": "Ідэнтыфікатар плагіна павінен змяшчаць не менш {{.Min}} сімвалаў, не больш за {{.Max}} сімвалаў і адпавядаць {{.Regex}}." + }, + { + "id": "app.plugin.install_marketplace_plugin.app_error", + "translation": "Не атрымалася ўсталяваць убудову з крамы плагінаў." + }, + { + "id": "app.plugin.install_id_failed_remove.app_error", + "translation": "Немагчыма ўсталяваць убудову. Убудова з тым жа ідэнтыфікатарам ужо ўсталяваны і не можа быць выдалены." + }, + { + "id": "app.plugin.install_id.app_error", + "translation": "Немагчыма ўсталяваць убудову. Убудова з такім жа ідэнтыфікатарам ужо ўсталяваны." + }, + { + "id": "app.plugin.install.app_error", + "translation": "Немагчыма ўсталяваць убудову." + }, + { + "id": "app.plugin.get_statuses.app_error", + "translation": "Немагчыма атрымаць статуты плагінаў." + }, + { + "id": "app.plugin.get_public_key.get_file.app_error", + "translation": "Адбылася памылка пры атрыманні адчыненага ключа са сховішча." + }, + { + "id": "app.plugin.get_plugins.app_error", + "translation": "Не атрымалася атрымаць актыўныя плагіны." + }, + { + "id": "app.plugin.get_cluster_plugin_statuses.app_error", + "translation": "Немагчыма атрымаць статуты плагінаў з кластара." + }, + { + "id": "app.plugin.flag_managed.app_error", + "translation": "Немагчыма задаць убудову як кіраваны файлавым сховішчам." + }, + { + "id": "app.plugin.extract.app_error", + "translation": "Выяўлена памылка пры выманні камплекта плагіна." + }, + { + "id": "app.plugin.disabled.app_error", + "translation": "Убудовы былі адключаныя. Калі ласка, праверце вашыя часопісы для падрабязнасьцяў." + }, + { + "id": "app.plugin.delete_public_key.delete.app_error", + "translation": "Узнікла памылка пры выдаленні адчыненага ключа." + }, + { + "id": "app.plugin.deactivate.app_error", + "translation": "Не атрымалася адключыць убудову." + }, + { + "id": "app.plugin.config.app_error", + "translation": "Памылка захавання стану плагіна ў канфігу." + }, + { + "id": "app.plugin.cluster.save_config.app_error", + "translation": "Канфігурацыя плагіна ў файле config.json павінна абнаўляцца ўручную пры выкарыстанні ReadOnlyConfig з уключанай кластэрызацыяй." + }, + { + "id": "app.oauth.update_app.updating.app_error", + "translation": "Мы сутыкнуліся з памылкай пры абнаўленні дадатку." + }, + { + "id": "app.oauth.update_app.find.app_error", + "translation": "Немагчыма знайсці існуючае прыкладанне для абнаўлення." + }, + { + "id": "app.oauth.save_app.save.app_error", + "translation": "Немагчыма захаваць дадатак." + }, + { + "id": "app.oauth.save_app.existing.app_error", + "translation": "Неабходна выклікаць абнаўленне для існуючага дадатку." + }, + { + "id": "app.oauth.remove_access_data.app_error", + "translation": "Немагчыма выдаліць токен доступу." + }, + { + "id": "app.oauth.permanent_delete_auth_data_by_user.app_error", + "translation": "Немагчыма выдаліць код аўтарызацыі." + }, + { + "id": "app.oauth.get_apps.find.app_error", + "translation": "Адбылася памылка пры пошуку прыкладанняў OAuth2." + }, + { + "id": "app.oauth.get_app_by_user.find.app_error", + "translation": "Мы не змаглі знайсці якія-небудзь існуючыя прыкладанні." + }, + { + "id": "app.oauth.get_app.finding.app_error", + "translation": "Мы знайшлі памылку пры пошуку прыкладання." + }, + { + "id": "app.oauth.get_app.find.app_error", + "translation": "Немагчыма знайсці апытанае прыкладанне." + }, + { + "id": "app.oauth.get_access_data_by_user_for_app.app_error", + "translation": "Мы сутыкнуліся з памылкай пры пошуку ўсіх токенаў доступу." + }, + { + "id": "app.oauth.delete_app.app_error", + "translation": "Адбылася памылка пры выдаленні прыкладання OAuth2." + }, + { + "id": "app.notification.subject.notification.full", + "translation": "[{{ .SiteName }}] Апавяшчэнне ў {{ .TeamName}} у {{.Month}} {{.Day}}, {{.Year}}" + }, + { + "id": "app.notification.subject.group_message.generic", + "translation": "[{{.SiteName}}] Новае групавое паведамленне ў {{.Day}} {{.Month}}, {{.Year}}" + }, + { + "id": "app.notification.subject.group_message.full", + "translation": "[{{ .SiteName }}] Новае групавое паведамленне на {{ .ChannelName}} у {{.Month}} {{.Day}}, {{.Year}}" + }, + { + "id": "app.notification.subject.direct.full", + "translation": "[{{.SiteName}}] Новае асабістае паведамленне ад @{{.SenderDisplayName}} у {{.Month}} {{.Day}}, {{.Year}}" + }, + { + "id": "app.notification.footer.title", + "translation": "Жадаеце змяніць параметры апавяшчэнняў?" + }, + { + "id": "app.notification.footer.info", + "translation": " і перайдзіце ў Параметры > Апавяшчэнні" + }, + { + "id": "api.templates.verify_body.info1", + "translation": "Калі гэта былі не вы, вы можаце смела ігнараваць гэты ліст." + }, + { + "id": "api.templates.verify_body.info", + "translation": "Гэты адрас электроннай пошты быў выкарыстаны для стварэння ўліковага запісу Mattermost." + }, + { + "id": "api.templates.username_change_subject", + "translation": "[{{ .SiteName }}] Ваша імя карыстальніка было зменена" + }, + { + "id": "api.templates.username_change_body.title", + "translation": "Вы абнавілі ваша імя карыстальніка" + }, + { + "id": "api.templates.username_change_body.info", + "translation": "Ваш e-mail адрас для {{.TeamDisplayName}} быў зменены на {{.NewEmail}}." + }, + { + "id": "api.templates.user_access_token_subject", + "translation": "[{{ .SiteName }}] Персанальны токен доступу дададзены да вашага акаўнта" + }, + { + "id": "api.templates.user_access_token_body.title", + "translation": "Асабісты ключ доступу, дададзены ў ваш уліковы запіс" + }, + { + "id": "api.templates.user_access_token_body.info", + "translation": "У ваш рахунак дададзены токен доступу для {{.SiteURL}}. Яго можна выкарыстоўваць для доступу да {{.SiteName}} з вашым уліковым запісам." + }, + { + "id": "api.templates.signin_change_email.subject", + "translation": "[{{ .SiteName }}] Ваш метад уваходу абноўлены" + }, + { + "id": "api.templates.signin_change_email.body.title", + "translation": "Вы абнавілі свой спосаб уваходу" + }, + { + "id": "api.templates.signin_change_email.body.info", + "translation": "Вы змянілі свой метад уваходу для {{.SiteName}} на {{.Method}}." + }, + { + "id": "api.templates.reset_subject", + "translation": "[{{ .SiteName }}] Скінуць пароль" + }, + { + "id": "api.templates.reset_body.subTitle", + "translation": "Націсніце на кнопку ніжэй, каб скінуць пароль. Калі вы не запытвалі яго, вы можаце бяспечна праігнараваць гэты ліст." + }, + { + "id": "api.templates.reset_body.info", + "translation": "Тэрмін дзеяння спасылкі скіду пароля заканчваецца праз 24 гадзіны." + }, + { + "id": "api.templates.remove_expired_license.subject", + "translation": "Карпаратыўная ліцэнзія Mattermost была адключаная." + }, + { + "id": "api.templates.remove_expired_license.body.title", + "translation": "Тэрмін дзеяння карпаратыўнай ліцэнзіі скончыўся, і некаторыя функцыі могуць быць адключаныя. Калі ласка, абнавіце ліцэнзію." + }, + { + "id": "api.templates.questions_footer.info", + "translation": "Напішыце нам у любы час на email " + }, + { + "id": "api.templates.payment_failed_no_card.title", + "translation": "Ваш рахунак за Mattermost Cloud да аплаты" + }, + { + "id": "api.templates.payment_failed_no_card.subject", + "translation": "Аплата робіцца за падпіску на Mattermost Cloud" + }, + { + "id": "api.templates.payment_failed_no_card.info3", + "translation": "Каб прагледзець рахунак і дадаць спосаб аплаты, абярыце Аплаціць зараз." + }, + { + "id": "api.templates.payment_failed_no_card.info1", + "translation": "Ваш рахунак Mattermost Cloud за апошні разліковы перыяд быў апрацаваны. Аднак у нас няма вашых плацёжных рэквізітаў." + }, + { + "id": "api.templates.payment_failed.subject", + "translation": "Патрабуецца дзеянне: Аплата за Mattermost Cloud не праведзена" + }, + { + "id": "api.templates.payment_failed.info3", + "translation": "Каб забяспечыць бесперабойную падпіску на Mattermost Cloud, звяжыцеся з вашай фінансавай установай для ўхілення асноўнай праблемы або абновіце аплатную інфармацыю. Пасля абнаўлення плацежных дадзеных Mattermost распачне спробу пагасіць рэшту запазычанасці." + }, + { + "id": "api.templates.payment_failed.info2", + "translation": "Яны прывялі наступную прычыну:" + }, + { + "id": "api.templates.payment_failed.info1", + "translation": "Ваша фінансавая ўстанова адхіліла аплату вашай карты {{.CardBrand}}. ****{{.LastFour}}, звязаны з вашым працоўным прасторай Mattermost Cloud." + }, + { + "id": "api.templates.password_change_subject", + "translation": "[{{ .SiteName }}] Ваш пароль абноўлены" + }, + { + "id": "api.templates.password_change_body.title", + "translation": "Ваш пароль быў абноўлены" + }, + { + "id": "api.templates.password_change_body.info", + "translation": "Ваш пароль быў абноўлены для {{.TeamDisplayName}} у {{.TeamURL}} {{.Method}}." + }, + { + "id": "api.templates.mfa_deactivated_body.title", + "translation": "Шматфактарная праверка сапраўднасці адключана" + }, + { + "id": "api.templates.mfa_deactivated_body.info", + "translation": "Шматфактарная аўтэнтыфікацыя была выдалена з вашага ўліковага запісу на {{.SiteURL}}." + }, + { + "id": "api.templates.mfa_change_subject", + "translation": "[{{ .SiteName }}] Ваша MFA была абноўлена" + }, + { + "id": "api.templates.mfa_activated_body.title", + "translation": "Шматфактарная праверка сапраўднасці актывавана" + }, + { + "id": "api.templates.mfa_activated_body.info", + "translation": "Шматфактарная аўтэнтыфікацыя была дададзена ў ваш уліковы запіс на {{.SiteURL}}." + }, + { + "id": "api.templates.license_up_for_renewal_title", + "translation": "Ваша падпіска Mattermost падлягае падаўжэнню" + }, + { + "id": "api.templates.license_up_for_renewal_subtitle_two", + "translation": "Увайдзіце ў свой уліковы запіс кліента, каб падоўжыць" + }, + { + "id": "api.templates.license_up_for_renewal_subtitle", + "translation": "{{.UserName}}, тэрмін дзеяння Вашай падпіскі заканчваецца праз {{.Days}} дзён. Мы спадзяемся, што вам падабаецца гнуткае і бяспечнае каманднае супрацоўніцтва, якое забяспечвае Mattermost. Падоўжыце падпіску хуткім часам, каб Ваша каманда магла працягваць карыстацца гэтымі перавагамі." + }, + { + "id": "api.templates.license_up_for_renewal_subject", + "translation": "Ваша ліцэнзія падлягае падаўжэнню" + }, + { + "id": "api.templates.invite_subject", + "translation": "[{{ .SiteName }}] {{ .SenderName }} запрасіў вас у каманду {{ .TeamDisplayName }}" + }, + { + "id": "api.templates.invite_guest_subject", + "translation": "[{{ .SiteName }}] {{ .SenderName }} запрасіў вас у каманду {{ .TeamDisplayName }} у якасці госця" + }, + { + "id": "api.templates.invite_body_guest.subTitle", + "translation": "Вы былі запрошаны ў якасці госця ў каманду" + }, + { + "id": "api.templates.invite_body_footer.info", + "translation": "Mattermost – гэта гнуткая платформа абмену паведамленнямі з адкрытым зыходным кодам, якая забяспечвае бяспечную сумесную працу ў камандзе." + }, + { + "id": "api.templates.invite_body.title", + "translation": "{{ .SenderName }} запрасіў вас далучыцца да каманды {{ .TeamDisplayName }}." + }, + { + "id": "api.templates.invite_body.subTitle", + "translation": "Пачніце працу з вашай камандай на Mattermost" + }, + { + "id": "api.templates.email_warning", + "translation": "Калі вы не ўносілі гэтыя змены, звернецеся да сістэмнага адміністратара." + }, + { + "id": "api.templates.email_us_anytime_at", + "translation": "Напішыце нам у любы час на email " + }, + { + "id": "api.templates.email_info1", + "translation": "Па любых пытаннях, пішыце нам у любы час: " + }, + { + "id": "api.templates.email_footer_v2", + "translation": "© 2022 Mattermost, Inc. 530 Lytton Avenue, Second floor, Palo Alto, CA, 94301" + }, + { + "id": "api.templates.email_footer", + "translation": "Каб змяніць налады апавяшчэнняў, увайдзіце ў сваю каманду і перайдзіце ў Параметры > Апавяшчэнні." + }, + { + "id": "api.templates.email_change_verify_subject", + "translation": "[{{ .SiteName }}] Пацвердзіць новы email" + }, + { + "id": "api.templates.email_change_verify_body.title", + "translation": "Вы абнавілі свой email" + }, + { + "id": "api.templates.email_change_verify_body.info", + "translation": "Для завяршэння абнаўлення Вашага адраса электроннай пошты для {{.TeamDisplayName}}, калі ласка, націсніце на спасылку ніжэй, каб пацвердзіць карэктнасць адраса." + }, + { + "id": "api.templates.email_change_subject", + "translation": "[{{ .SiteName }}] Ваш email быў зменены" + }, + { + "id": "api.templates.email_change_body.title", + "translation": "Вы абнавілі свой email" + }, + { + "id": "api.templates.email_change_body.info", + "translation": "Ваш e-mail адрас для {{.TeamDisplayName}} быў зменены на {{.NewEmail}}." + }, + { + "id": "api.templates.deactivate_subject", + "translation": "[{{ .SiteName }}] Ваш рахунак на {{ .ServerURL }} быў дэактываваны" + }, + { + "id": "api.templates.deactivate_body.warning", + "translation": "Калі гэтая змена не была ініцыяваная вамі ці вы жадаеце паўторна актываваць свой уліковы запіс, звярніцеся да сістэмнага адміністратара." + }, + { + "id": "api.server.warn_metric.number_of_channels_50.start_trial.notification_body", + "translation": "Каналы дапамагаюць палепшыць камунікацыю, але па меры далучэння да Mattermost новых карыстальнікаў і стварэння імі каналаў, задача падтрымання арганізаванасці сістэмы ўскладняецца. Пашыраныя правы дазваляюць устанавіць, якія карыстальнікі або ролі могуць выконваць пэўныя дзеянні, у тым ліку кіраванне настройкамі каналаў і падпісчыкаў, выкарыстоўваючы тэгі @channel або @here для шырокіх груп карыстальнікаў, а таксама для стварэння новых падзей webhook.\n\n[Больш падрабязна аб выкарыстанні пашыраных правоў] (https://www.mattermost.com/docs-advanced-permissions/?utm_medium=product&utm_source=mattermost-advisor-bot&utm_content=advanced-permissions).\n\nНаціснуўшы кнопку \"Пачаць выкарыстоўваць пробную версію\", я згаджаюся з [Пагадненнем аб ацэнцы праграмнага забеспячэння Mattermost](https://mattermost.com/software-evaluation-agreement/), [Палітыкай прыватнасці](https://mattermost.com/ privacy-policy/) і атрымліваю паведамленні аб прадукце па email." + }, + { + "id": "api.server.warn_metric.number_of_channels_50.notification_body", + "translation": "Каналы дапамагаюць палепшыць камунікацыю, але па меры далучэння да Mattermost новых карыстальнікаў і стварэння імі каналаў, задача падтрымання арганізаванасці сістэмы ўскладняецца. Пашыраныя правы дазваляюць устанавіць, якія карыстальнікі або ролі могуць выконваць пэўныя дзеянні, у тым ліку кіраванне настройкамі каналаў і падпісчыкаў, выкарыстоўваючы тэгі @channel або @here для шырокіх груп карыстальнікаў, а таксама для стварэння новых падзей webhook.\n\n[Больш падрабязна аб выкарыстанні пашыраных правоў] (https://www.mattermost.com/docs-advanced-permissions/?utm_medium=product&utm_source=mattermost-advisor-bot&utm_content=advanced-permissions).\n\nНаціснуўшы кнопку \"Звязацца з намі\", вы падзяліцеся сваёй інфармацыяй з кампаніяй Mattermost, Inc. [Больш падрабязна] (https://mattermost.com/pl/default-admin-advisory)" + }, + { + "id": "api.server.warn_metric.number_of_channels_50.contact_us.email_body", + "translation": "Запыт кантакту з Mattermost. Мне цікава даведацца больш аб выкарыстанні пашыраных дазволаў з сістэмнымі схемамі.\n" + }, + { + "id": "api.server.warn_metric.number_of_active_users_500.start_trial.notification_success.message", + "translation": "Ваша пробная версія Enterprise зараз актыўная. Перайдзіце ў Сістэмную кансоль, каб уключыць пашыраныя функцыі." + }, + { + "id": "api.server.warn_metric.number_of_active_users_500.start_trial.notification_body", + "translation": "Пры разгортванні Mattermost больш, чым на 500 карыстальнікаў, мы настойліва раім выкарыстоўваць такія функцыі, як кіраванне карыстальнікамі, кластарызацыя сервераў і маніторынг прадукцыйнасці. Звяжыцеся з намі, каб пазнаць больш і дайце ведаць, чым мы можам дапамагчы.\n\nНаціснуўшы кнопку \"Пачаць выкарыстоўваць пробную версію\", я згаджаюся з [Пагадненнем аб ацэнцы праграмнага забеспячэння Mattermost](https://mattermost.com/software-evaluation-agreement/), [Палітыкай прыватнасці](https://mattermost.com/ privacy-policy/) і атрымліваю паведамленні аб прадукце па email." + }, + { + "id": "api.server.warn_metric.number_of_active_users_500.notification_body", + "translation": "Mattermost настойліва рэкамендуе, каб пры разгортванні больш, чым на 500 карыстальнікаў было зроблена абнаўленне да Карпаратыўнай рэдакцыі Mattermost, якая прапануе такія функцыі, як кіраванне карыстальнікамі, кластарызацыя сервераў і маніторынг прадукцыйнасці. Звяжыцеся з намі, каб пазнаць больш і дайце ведаць, чым мы можам дапамагчы.\n\nНаціснуўшы кнопку Звязацца з намі, вы падзяліцеся сваёй інфармацыяй з Mattermost, Inc. [Больш падрабязна] (https://mattermost.com/pl/default-admin-advisory)" + }, + { + "id": "api.server.warn_metric.number_of_active_users_500.contact_us.email_body", + "translation": "запыт Mattermost Звяжыцеся з намі. У маёй каманды зараз {{.Limit}} карыстальнікаў, і я разглядаю Mattermost Карпаратыўная рэдакцыя.\n" + }, + { + "id": "api.server.warn_metric.number_of_active_users_300.start_trial.notification_success.message", + "translation": "Ваша пробная версія Enterprise зараз актыўная. Стварыце канал і перайдзіце да **Сістэмная кансоль > Упраўленне карыстальнікамі > Каналы**, каб усталяваць ліміты публікацый для адміністратараў каналаў." + }, + { + "id": "api.server.warn_metric.number_of_active_users_300.start_trial.notification_body", + "translation": "У Mattermost шмат перамоваў, таму можа быць складана зразумець, дзе шукаць важную інфармацыю. Калі вы жадаеце трансляваць паведамленне для вялікай аўдыторыі, вы можаце наладзіць каналы аб'яваў для чытання, да якіх можа далучыцца любы ахвочы, але толькі адміністратары каналаў могуць размяшчаць паведамленні.\n\n[Больш падрабязна аб стварэнні каналаў аб'яваў толькі для чытання] (https://www.mattermost.com/docs-channel-moderation/?utm_medium=product&utm_source=mattermost-advisor-bot&utm_content=channel-moderation).\n\nНаціснуўшы кнопку \"Пачаць выкарыстоўваць пробную версію\", я згаджаюся з [Пагадненнем аб ацэнцы праграмнага забеспячэння Mattermost](https://mattermost.com/software-evaluation-agreement/), [Палітыкай прыватнасці](https://mattermost.com/ privacy-policy/) і атрымліваю паведамленні аб прадукце па email." + }, + { + "id": "api.server.warn_metric.number_of_active_users_300.notification_body", + "translation": "У Mattermost шмат перамоваў, таму можа быць складана зразумець, дзе шукаць важную інфармацыю. Калі вы жадаеце трансляваць паведамленне для вялікай аўдыторыі, вы можаце наладзіць каналы аб'яваў для чытання, да якіх можа далучыцца любы ахвочы, але толькі адміністратары каналаў могуць размяшчаць паведамленні.\n\n[Больш падрабязна аб стварэнні каналаў аб'яваў толькі для чытання] (https://www.mattermost.com/docs-channel-moderation/?utm_medium=product&utm_source=mattermost-advisor-bot&utm_content=channel-moderation).\n\nНаціснуўшы кнопку \"Звязацца з намі\", вы падзяліцеся сваёй інфармацыяй з кампаніяй Mattermost, Inc. [Даведацца больш] (https://mattermost.com/pl/default-admin-advisory)" + }, + { + "id": "api.server.warn_metric.number_of_active_users_300.contact_us.email_body", + "translation": "Запыт кантакту з Mattermost. Мне цікава даведацца больш аб стварэнні каналаў аб'яваў толькі для.\n" + }, + { + "id": "api.server.warn_metric.number_of_active_users_200.start_trial.notification_success.message", + "translation": "Ваша пробная версія Enterprise зараз актыўная. Перайдзіце ў **Сістэмная кансоль > Аўтэнтыфікацыя > SAML 2.0** для інтэграцыі з правайдэрам SAML 2.0." + }, + { + "id": "api.server.warn_metric.number_of_active_users_200.start_trial.notification_body", + "translation": "У вашай сістэме Mattermost зараз 200 карыстальнікаў. Калі вы падключаеце Mattermost да пастаўшчыка сістэмы адзінага ўваходу ў вашай арганізацыі, карыстачы могуць атрымаць доступ да Mattermost без неабходнасці паўторна ўводзіць свае ўліковыя дадзеныя. Мы раім вам інтэграваць свайго правайдэра SAML 2.0 з серверам Mattermost. [Больш падрабязна аб інтэграцыі з SAML 2.0] (https://www.mattermost.com/docs-saml/?utm_medium=product&utm_source=mattermost-advisor-bot&utm_content=saml)\n\nНаціскаючы \"Паспрабаваць\", я прымаю [Пагадненне аб ацэнцы праграмнага забеспячэння Mattermost](https://mattermost.com/software-evaluation-agreement/), [Палітыку прыватнасці](https://mattermost.com/privacy-policy/) і гатовы атрымліваць электронныя лісты аб прадуктах." + }, + { + "id": "api.server.warn_metric.number_of_active_users_200.notification_body", + "translation": "У вашай сістэме Mattermost зараз 200 карыстальнікаў. Калі вы падключаеце Mattermost да пастаўшчыка сістэмы адзінага ўваходу ў вашай арганізацыі, карыстачы могуць атрымаць доступ да Mattermost без неабходнасці паўторна ўводзіць свае ўліковыя дадзеныя. Мы раім вам інтэграваць свайго пастаўшчыка SAML 2.0 з вашым серверам Mattermost. [Больш падрабязна аб інтэграцыі з SAML 2.0](https://www.mattermost.com/docs-saml/?utm_medium=product&utm_source=mattermost-advisor-bot&utm_content=saml) .\n\nНаціснуўшы «Звязацца з намі», вы падзяліцеся сваёй інфармацыяй з Mattermost, Inc. [Больш падрабязна](https://mattermost.com/pl/default-admin-advisory)" + }, + { + "id": "api.server.warn_metric.number_of_active_users_200.contact_us.email_body", + "translation": "запыт Mattermost Звяжыцеся з намі. У маёй каманды зараз {{.Limit}} карыстальнікаў, і я разглядаю Mattermost Карпаратыўная рэдакцыя.\n" + }, + { + "id": "api.server.warn_metric.number_of_active_users_100.start_trial.notification_success.message", + "translation": "Ваша пробная версія Enterprise зараз актыўная. Перайдзіце ў **Сістэмная кансоль > Аўтэнтыфікацыя > AD/LDAP** для інтэграцыі службы AD/LDAP." + }, + { + "id": "api.server.warn_metric.number_of_active_users_100.start_trial.notification_body", + "translation": "У вашай сістэмы Mattermost больш за 100 карыстальнікаў. Па меры росту вашай базы карыстальнікаў, стварэнне новых уліковых запісаў можа забіраць шмат часу. Мы рэкамендуем інтэграваць Active Directory/LDAP вашай арганізацыі, што дазволіць любому, у каго ёсць уліковы запіс, атрымаць доступ да Mattermost.\n\n[Больш падрабязна аб інтэграцыі з AD/LDAP](https://www.mattermost.com/docs-adldap/?utm_medium=product&utm_source=mattermost-advisor-bot&utm_content=adldap)\n\nНаціснуўшы кнопку \"Пачаць выкарыстоўваць пробную версію\", я згаджаюся з [Пагадненнем аб ацэнцы праграмнага забеспячэння Mattermost](https://mattermost.com/software-evaluation-agreement/), [Палітыкай прыватнасці](https://mattermost.com/ privacy-policy/) і атрымліваю паведамленні аб прадукце па email." + }, + { + "id": "api.server.warn_metric.number_of_active_users_100.notification_body", + "translation": "У вашай сістэме Mattermost зараз 200 карыстальнікаў. Па меры росту вашай карыстацкай базы стварэнне новых уліковых запісаў можа заняць шмат часу. Мы рэкамендуем вам інтэграваць Active Directory/LDAP вашай арганізацыі, што дазволіць любому, у каго ёсць уліковы запіс, атрымаць доступ да Mattermost.\n\n[Больш падрабязна аб інтэграцыі з AD/LDAP](https://docs.mattermost.com/deployment/sso-ldap.html?utm_medium=product&utm_source=mattermost-advisor-bot&utm_content=adldap)\n\nНаціснуўшы «Пацвердзіць», вы падзяліцеся сваёй інфармацыяй з Mattermost Inc., каб даведацца больш аб абнаўленні. [Больш падрабязна…](https://mattermost.com/pl/default-admin-advisory)" + }, + { + "id": "api.server.warn_metric.number_of_active_users_100.contact_us.email_body", + "translation": "Запыт кантакту з Mattermost. У маёй каманды зараз 100 карыстальнікаў, і я разглядаю Карпаратыўную рэдакцыю Mattermost.\n" + }, + { + "id": "api.server.warn_metric.mfa.start_trial_notification_success.message", + "translation": "Ваша пробная Карпаратыўная версія зараз актыўная. Перайдзіце ў **Сістэмная кансоль > Аўтэнтыфікацыя > МФА**, каб уключыць шматфактарную аўтэнтыфікацыю." + }, + { + "id": "api.server.warn_metric.mfa.start_trial.notification_body", + "translation": "У вашай сістэме Mattermost уключана шматфактарная аўтэнтыфікацыя, што дае карыстальнікам магчымасць абараніць свае ўліковыя запісы з дапамогай дадатковых сродкаў аўтэнтыфікацыі акрамя пароля. Для павышэння бяспекі ўсёй сістэмы вы можаце запатрабаваць, каб усе ўліковыя запісы Mattermost выкарыстоўвалі шматфактарную аўтэнтыфікацыю.\n\n[Больш падрабязна аб прымяненні Шматфактарнай аўтэнтыфікацыі](https://www.mattermost.com/docs-multi-factor-authentication/?utm_medium=product&utm_source=mattermost-advisor-bot&utm_content=multi-factor-authentication)\n\nНаціскаючы Пачаць пробную версію, я прымаю [Пагадненне аб ацэнцы праграмнага забеспячэння Mattermost](https://mattermost.com/software-evaluation-agreement/), [Палітыку прыватнасці](https://mattermost.com/privacy-policy/) і гатовы атрымліваць электронныя лісты аб прадуктах." + }, + { + "id": "api.server.warn_metric.mfa.notification_body", + "translation": "У вашай сістэме Mattermost уключана шматфактарная аўтэнтыфікацыя, што дае карыстальнікам магчымасць абараніць свае ўліковыя запісы з дапамогай дадатковых сродкаў аўтэнтыфікацыі акрамя пароля. Для павышэння бяспекі ўсёй сістэмы вы можаце запатрабаваць, каб усе ўліковыя запісы Mattermost выкарыстоўвалі шматфактарную аўтэнтыфікацыю.\n\n[Больш падрабязна аб прымусовай шматфактарнай аўтэнтыфікацыі](https://www.mattermost.com/docs-multi-factor-authentication/?utm_medium=product&utm_source=mattermost-advisor-bot&utm_content=multi-factor-authentication).\n\nНаціснуўшы «Звязацца з намі», вы падзяліцеся сваёй інфармацыяй з Mattermost, Inc. [Больш падрабязна](https://mattermost.com/pl/default-admin-advisory)" + }, + { + "id": "api.server.warn_metric.mfa.contact_us.email_body", + "translation": "Просьба да Mattermost звязацца з намі. Мне цікава даведацца больш аб уключэнні шматфактарнай аўтэнтыфікацыі.\n" + }, + { + "id": "api.server.warn_metric.email_domain.start_trial_notification_success.message", + "translation": "Ваша пробная Карпаратыўная версія зараз актыўная. Перайдзіце ў **Сістэмная кансоль > Аўтэнтыфікацыя > Гасцявы доступ**, каб уключыць гасцявыя ўліковыя запісы." + }, + { + "id": "api.server.warn_metric.email_domain.start_trial.notification_body", + "translation": "У праектах часта ўдзельнічаюць людзі як унутры, так і за межамі арганізацыі. З дапамогай гасцявых уліковых запісаў вы можаце прыцягваць вонкавых партнёраў да сваёй сістэмы Mattermost і паказваць, з кім яны могуць працаваць і што яны могуць бачыць.\n\n[Больш падрабязна аб уключэнні гасцявых уліковых запісаў](https://www.mattermost.com/docs-guest-accounts/?utm_medium=product&utm_source=mattermost-advisor-bot&utm_content=guest-accounts)\n\nНаціскаючы \"Паспрабаваць\", я прымаю [Пагадненне аб ацэнцы праграмнага забеспячэння Mattermost](https://mattermost.com/software-evaluation-agreement/), [Палітыку прыватнасці](https://mattermost.com/privacy-policy/) і буду атрымліваць электронныя лісты аб прадуктах." + }, + { + "id": "api.server.warn_metric.email_domain.notification_body", + "translation": "У праектах часта ўдзельнічаюць людзі як унутры, так і за межамі арганізацыі. З дапамогай гасцявых уліковых запісаў вы можаце прыцягваць вонкавых партнёраў да сваёй сістэмы Mattermost і паказваць, з кім яны могуць працаваць і што яны могуць бачыць.\n\n[Больш падрабязна аб уключэнні гасцявых уліковых запісаў](https://www.mattermost.com/docs-guest-accounts/?utm_medium=product&utm_source=mattermost-advisor-bot&utm_content=guest-accounts).\n\nНаціснуўшы «Звязацца з намі», вы падзяліцеся сваёй інфармацыяй з Mattermost, Inc. [Больш падрабязна](https://mattermost.com/pl/default-admin-advisory)" + }, + { + "id": "api.server.warn_metric.email_domain.contact_us.email_body", + "translation": "Просьба да Mattermost звязацца з намі. Мне цікава даведацца больш аб выкарыстанні гасцявых уліковых запісаў.\n" + }, + { + "id": "api.server.warn_metric.bot_response.start_trial_failure.message", + "translation": "Не ўдалося атрымаць пробную ліцэнзію. Наведайце https://mattermost.com/trial/, каб запытаць ліцэнзію." + }, + { + "id": "api.server.warn_metric.bot_response.notification_success.message", + "translation": "Дзякуй, што звязаліся з Mattermost. Мы звяжамся з вамі ў бліжэйшы час." + }, + { + "id": "api.server.warn_metric.bot_response.notification_failure.message", + "translation": "Паведамленне не можа быць адпраўлена." + }, + { + "id": "api.server.warn_metric.bot_response.mailto_subject", + "translation": "Запыт Mattermost Звяжыцеся з намі" + }, + { + "id": "api.server.warn_metric.bot_response.mailto_registered_users_header", + "translation": "Усяго актыўных карыстальнікаў: {{.NoRegisteredUsers}}" + }, + { + "id": "api.server.warn_metric.bot_response.mailto_footer", + "translation": "Калі ў вас ёсць якія-небудзь дадатковыя пытанні, калі ласка, звяжыцеся з support@mattermost.com" + }, + { + "id": "api.server.start_server.starting.critical", + "translation": "Памылка запуску сервера, err:%v" + }, + { + "id": "api.server.start_server.rate_limiting_rate_limiter", + "translation": "Немагчыма ініцыялізаваць абмежаванне хуткасці." + }, + { + "id": "api.server.start_server.rate_limiting_memory_store", + "translation": "Не ўдалося ініцыялізаваць абмежаванне памяці. Праверце параметр MemoryStoreSize у наладах." + }, + { + "id": "api.server.start_server.forward80to443.enabled_but_listening_on_wrong_port", + "translation": "Не ўдаецца пераправіць порт 80 на порт 443 падчас праслухоўвання порта %s: адключыце пераадрасацыю порта 80 на 443 пры выкарыстанні проксі-сервера" + }, + { + "id": "api.server.start_server.forward80to443.disabled_while_using_lets_encrypt", + "translation": "Неабходна перанакіроўваць 80 на 443 пры выкарыстанні LetsEncrypt" + }, + { + "id": "api.server.license_up_for_renewal.error_sending_email", + "translation": "Не ўдалося адправіць ліцэнзію для абнаўленняў па электроннай пошце" + }, + { + "id": "api.server.license_up_for_renewal.error_generating_link", + "translation": "Не атрымалася згенераваць спасылку для падаўжэння ліцэнзіі" + }, + { + "id": "api.scheme.patch_scheme.license.error", + "translation": "Ваша ліцэнзія не падтрымлівае змену схем дазволу" + }, + { + "id": "api.scheme.get_teams_for_scheme.scope.error", + "translation": "Немагчыма атрымаць каманды для схемы, бо дадзеныя схемы не з'яўляюцца схемамі каманд." + }, + { + "id": "api.scheme.get_channels_for_scheme.scope.error", + "translation": "Немагчыма атрымаць каналы для схемы, бо прадстаўленыя схемы не з'яўляюцца схемамі каналаў." + }, + { + "id": "api.scheme.delete_scheme.license.error", + "translation": "Ваша ліцэнзія не падтрымлівае выдаленне схем дазволу" + }, + { + "id": "api.scheme.create_scheme.license.error", + "translation": "Ваша ліцэнзія не падтрымлівае стварэнне схем дазволу." + }, + { + "id": "api.roles.patch_roles.not_allowed_permission.error", + "translation": "Адно ці некалькі з дазволаў, якія вы спрабуеце дадаць ці выдаліць, не дадзена" + }, + { + "id": "api.roles.patch_roles.license.error", + "translation": "Ваша ліцэнзія не падтрымлівае пашыраныя дазволы." + }, + { + "id": "api.restricted_system_admin", + "translation": "Гэта дзеянне забаронена сістэмнаму адміністратару з абмежаванымі правамі." + }, + { + "id": "api.remote_cluster.update_not_unique.app_error", + "translation": "Бяспечнае злучэнне з такім URL ужо існуе." + }, + { + "id": "api.remote_cluster.update.app_error", + "translation": "Мы сутыкнуліся з памылкай пры абнаўленні бяспечнага злучэння." + }, + { + "id": "api.remote_cluster.service_not_enabled.app_error", + "translation": "Служба выдаленага кластара не ўключана." + }, + { + "id": "api.remote_cluster.save_not_unique.app_error", + "translation": "Бяспечнае злучэнне ўжо дададзена." + }, + { + "id": "api.remote_cluster.save.app_error", + "translation": "Адбылася памылка пры захаванні бяспечнага злучэння." + }, + { + "id": "api.remote_cluster.get.app_error", + "translation": "Мы сутыкнуліся з памылкай пры атрыманні бяспечнага злучэння." + }, + { + "id": "api.remote_cluster.delete.app_error", + "translation": "Узнікла памылка выдалення бяспечнага злучэння." + }, + { + "id": "api.reaction.save_reaction.user_id.app_error", + "translation": "Вы не можаце захаваць рэакцыю для іншага карыстальніка." + }, + { + "id": "api.reaction.save_reaction.invalid.app_error", + "translation": "Рэакцыя невалідная." + }, + { + "id": "api.reaction.save.archived_channel.app_error", + "translation": "Вы не можаце рэагаваць у архіўным канале." + }, + { + "id": "api.reaction.delete.archived_channel.app_error", + "translation": "Вы не можаце выдаліць рэакцыю ў архіўным канале." + }, + { + "id": "api.push_notifications_ack.message.parse.app_error", + "translation": "Адбылася памылка падчас пабудовы пацвярджальнага паведамлення push-паведамлення." + }, + { + "id": "api.push_notifications_ack.forward.app_error", + "translation": "Адбылася памылка пры адпраўцы паведамлення ў службу push-паведамленняў." + }, + { + "id": "api.push_notifications.session.expired", + "translation": "Час сеансу скончыўся: увайдзіце ў сістэму, каб працягнуць атрымліваць апавяшчэнні. Сеансы для {{.siteName}} настроены вашым сістэмным адміністратарам так, што тэрмін іх дзеяння заканчваецца кожныя {{.daysCount}} дзён." + }, + { + "id": "api.push_notifications.message.parse.app_error", + "translation": "Адбылася памылка падчас пабудовы паведамлення push-паведамлення." + }, + { + "id": "api.push_notification.id_loaded.fetch.app_error", + "translation": "Адбылася памылка пры атрыманні push-паведамлення з усталяваным ідэнтыфікатарам (ID Loaded)." + }, + { + "id": "api.push_notification.id_loaded.default_message", + "translation": "Вы атрымалі новае паведамленне." + }, + { + "id": "api.push_notification.disabled.app_error", + "translation": "Push апавяшчэння адключаныя на гэтым серверы." + }, + { + "id": "api.preference.update_preferences.update_sidebar.app_error", + "translation": "Немагчыма абнавіць бакавую панэль, каб адпавядаць абноўленым наладам" + }, + { + "id": "api.preference.update_preferences.set.app_error", + "translation": "Немагчыма ўсталяваць налады карыстальніка." + }, + { + "id": "api.preference.preferences_category.get.app_error", + "translation": "Немагчыма атрымаць наладкі карыстальніка." + }, + { + "id": "api.preference.delete_preferences.update_sidebar.app_error", + "translation": "Немагчыма абнавіць бакавую панэль, каб адпавядаць выдаленым наладам" + }, + { + "id": "api.preference.delete_preferences.delete.app_error", + "translation": "Немагчыма выдаліць наладкі карыстальніка." + }, + { + "id": "api.post_get_post_by_id.get.app_error", + "translation": "Не ўдалося атрымаць паведамленне." + }, + { + "id": "api.post.update_post.system_message.app_error", + "translation": "Не ўдалося абнавіць сістэмнае паведамленне." + }, + { + "id": "api.post.update_post.permissions_time_limit.app_error", + "translation": "Рэдагаванне паведамленняў дазволена толькі {{.timeLimit}} секунд пасля публікацыі. За падрабязнасцямі звярніцеся да Вашага сістэмнага адміністратара." + }, + { + "id": "api.post.update_post.find.app_error", + "translation": "Не ўдалося знайсці існуючае паведамленне або каментар для абнаўлення." + }, + { + "id": "api.post.update_post.can_not_update_post_in_deleted.error", + "translation": "Нельга абнавіць паведамленне ў выдаленым канале." + }, + { + "id": "api.post.send_notifications_and_forget.push_message", + "translation": "адправіў вам паведамленне." + }, + { + "id": "api.post.send_notification_and_forget.push_comment_on_thread", + "translation": " пракаментаваў нітку з вашым удзелам." + }, + { + "id": "api.post.send_notification_and_forget.push_comment_on_post", + "translation": " пракаментаваў ваш пост." + }, + { + "id": "api.post.send_notification_and_forget.push_comment_on_crt_thread", + "translation": " адказаў у чаце, на які Вы падпісаны." + }, + { + "id": "api.post.search_posts.invalid_body.app_error", + "translation": "Немагчыма прааналізаваць цела запыту." + }, + { + "id": "api.post.search_files.invalid_body.app_error", + "translation": "Не ўдалося разабраць цела запыту." + }, + { + "id": "api.post.patch_post.can_not_update_post_in_deleted.error", + "translation": "Нельга абнавіць паведамленне ў выдаленым канале." + }, + { + "id": "api.post.link_preview_disabled.app_error", + "translation": "Прадпрагляд спасылак быў забаронены сістэмным адміністратарам." + }, + { + "id": "api.post.get_message_for_notification.images_sent", + "translation": { + "few": "{{.Count}} выявы адпраўлена: {{.Filenames}}", + "many": "{{.Count}} выяў адпраўлена: {{.Filenames}}", + "one": "{{.Count}} выява адпраўлена: {{.Filenames}}" + } + }, + { + "id": "api.post.get_message_for_notification.files_sent", + "translation": { + "few": "", + "many": "", + "one": "{{.Count}} файл адпраўлены: {{.Filenames}}" + } + }, + { + "id": "api.post.error_get_post_id.pending", + "translation": "Немагчыма атрымаць паведамленне, якое чакае." + }, + { + "id": "api.post.disabled_here", + "translation": "Згадка @here была адключаная, паколькі колькасць карыстальнікаў на канале перавышае {{.Users}}." + }, + { + "id": "api.post.disabled_channel", + "translation": "@channel быў адключаны, паколькі колькасць карыстальнікаў перавышае {{.Users}}." + }, + { + "id": "api.post.disabled_all", + "translation": "Згадка @all была адключаная, паколькі колькасць карыстальнікаў на канале перавышае {{.Users}}." + }, + { + "id": "api.post.delete_post.can_not_delete_post_in_deleted.error", + "translation": "Нельга выдаліць пост у выдаленым канале." + }, + { + "id": "api.post.deduplicate_create_post.pending", + "translation": "Адпраўка паведамлення адхілена, т.я. іншы кліент зрабіў такі ж запыт." + }, + { + "id": "api.post.deduplicate_create_post.failed_to_get", + "translation": "Не ўдалося атрымаць арыгінальнае паведамленне. Кліент стварыў дублюючы запыт." + }, + { + "id": "api.post.create_post.channel_root_id.app_error", + "translation": "Няверны ChannelId для параметра RootId." + }, + { + "id": "api.post.create_post.can_not_post_to_deleted.error", + "translation": "Немагчыма стварыць пост у выдаленым канале." + }, + { + "id": "api.post.check_for_out_of_channel_mentions.message.one", + "translation": "@{{.Username}} не атрымаў паведамлення аб гэтай згадцы, паколькі яго няма ў канале." + }, + { + "id": "api.post.check_for_out_of_channel_mentions.message.multiple", + "translation": "@{{.Usernames}} і @{{.LastUsername}} не атрымалі апавяшчэння аб гэтай згадцы, паколькі іх няма ў канале." + }, + { + "id": "api.post.check_for_out_of_channel_groups_mentions.message.one", + "translation": "@{{.Usernames}} не апавешчаныя гэтай згадкай, таму што яны не з'яўляюцца ўдзельнікамі канала. Яны не могуць быць дададзены ў канал, таму што яны не з'яўляюцца ўдзельнікамі звязаных груп. Каб дадаць іх у канал, яны павінны быць дададзены ў звязаныя групы." + }, + { + "id": "api.post.check_for_out_of_channel_groups_mentions.message.multiple", + "translation": "@{{.Usernames}} і @{{.LastUsername}} не апавешчаныя гэтай згадкай, таму што яны не з'яўляюцца ўдзельнікамі канала. Яны не могуць быць дададзены ў канал, таму што яны не з'яўляюцца ўдзельнікамі звязаных груп. Каб дадаць іх у канал, яны павінны быць дададзены ў звязаныя групы." + }, + { + "id": "api.post.check_for_out_of_channel_group_users.message.none", + "translation": "@{{.GroupName}} не мае ўдзельнікаў у гэтай камандзе" + }, + { + "id": "api.plugin.verify_plugin.app_error", + "translation": "Немагчыма праверыць подпіс плагіна." + }, + { + "id": "api.plugin.upload.no_file.app_error", + "translation": "Адсутнічае файл у запыце тыпу multipart/form." + }, + { + "id": "api.plugin.upload.file.app_error", + "translation": "Немагчыма адкрыць файл у запыце multipart/form." + }, + { + "id": "api.plugin.upload.array.app_error", + "translation": "Файлавы масіў пусты ў запыце multipart/form." + }, + { + "id": "api.plugin.install.download_failed.app_error", + "translation": "Пры спампоўцы плагіна адбылася памылка." + }, + { + "id": "api.outgoing_webhook.disabled.app_error", + "translation": "Выходныя вэбхукі адключаныя сістэмным адміністратарам." + }, + { + "id": "api.oauth.singup_with_oauth.invalid_link.app_error", + "translation": "Спасылка для рэгістрацыі, здаецца, няправільная." + }, + { + "id": "api.oauth.singup_with_oauth.expired_link.app_error", + "translation": "Спасылка для рэгістрацыі састарэлая." + }, + { + "id": "api.oauth.singup_with_oauth.disabled.app_error", + "translation": "Рэгістрацыя карыстальнікаў адключана." + }, + { + "id": "api.oauth.revoke_access_token.get.app_error", + "translation": "Памылка атрымання токена з БД перад выдаленнем." + }, + { + "id": "api.oauth.revoke_access_token.del_token.app_error", + "translation": "Памылка выдалення токена з БД." + }, + { + "id": "api.oauth.revoke_access_token.del_session.app_error", + "translation": "Памылка выдалення сесіі з БД." + }, + { + "id": "api.oauth.register_oauth_app.turn_off.app_error", + "translation": "Сістэмны адміністратар адключыў пастаўшчыка паслуг OAuth2." + }, + { + "id": "api.oauth.redirecting_back", + "translation": "Перанакіраванне назад у дадатак." + }, + { + "id": "api.oauth.get_access_token.refresh_token.app_error", + "translation": "invalid_grant: Памылковы токен абнаўлення." + }, + { + "id": "api.oauth.get_access_token.redirect_uri.app_error", + "translation": "invalid_request: Прадастаўлены redirect_url не адпавядае redirect_url кода аўтарызацыі." + }, + { + "id": "api.oauth.get_access_token.internal_user.app_error", + "translation": "server_error: Адбылася ўнутраная памылка сервера падчас вымання карыстальніка з базы дадзеных." + }, + { + "id": "api.oauth.get_access_token.internal_session.app_error", + "translation": "server_error: Адбылася ўнутраная памылка сервера падчас захавання сесіі ў базу дадзеных." + }, + { + "id": "api.oauth.get_access_token.internal_saving.app_error", + "translation": "server_error: Адбылася ўнутраная памылка сервера падчас захавання токена ў базу дадзеных." + }, + { + "id": "api.oauth.get_access_token.internal.app_error", + "translation": "server_error: Адбылася ўнутраная памылка сервера падчас доступу да базы дадзеных." + }, + { + "id": "api.oauth.get_access_token.expired_code.app_error", + "translation": "invalid_grant: Несапраўдны ці пратэрмінаваны код аўтарызацыі." + }, + { + "id": "api.oauth.get_access_token.disabled.app_error", + "translation": "Сістэмны адміністратар адключыў пастаўшчыка паслуг OAuth2." + }, + { + "id": "api.oauth.get_access_token.credentials.app_error", + "translation": "invalid_client: Няправільныя ўліковыя дадзеныя кліента." + }, + { + "id": "api.oauth.close_browser", + "translation": "Цяпер Вы можаце закрыць гэтую ўкладку браўзэра." + }, + { + "id": "api.oauth.click_redirect", + "translation": "Калі Вы не былі перанакіраваны аўтаматычна, націсніце link" + }, + { + "id": "api.oauth.authorize_oauth.disabled.app_error", + "translation": "Сістэмны адміністратар адключыў пастаўшчыка паслуг OAuth2." + }, + { + "id": "api.oauth.allow_oauth.turn_off.app_error", + "translation": "Сістэмны адміністратар адключыў пастаўшчыка паслуг OAuth2." + }, + { + "id": "api.oauth.allow_oauth.redirect_callback.app_error", + "translation": "invalid_request: Прадастаўлены redirect_uri не адпавядае зарэгістраванаму callback_url." + }, + { + "id": "api.no_license", + "translation": "Для выкарыстання гэтага endpoint патрабуецца ліцэнзія E10 ці E20." + }, + { + "id": "api.migrate_to_saml.error", + "translation": "Немагчыма перанесці SAML." + }, + { + "id": "api.license.upgrade_needed.app_error", + "translation": "Для гэтай функцыі патрабуецца абнаўленне да Enterprise Edition." + }, + { + "id": "api.license.request_trial_license.fail_get_user_count.app_error", + "translation": "Не ўдалося атрымаць пробную ліцэнзію, паспрабуйце яшчэ раз ці зьвяжыцеся з support@mattermost.com. Немагчыма атрымаць колькасць зарэгістраваных карыстальнікаў." + }, + { + "id": "api.license.request_trial_license.app_error", + "translation": "Не ўдалося атрымаць пробную ліцэнзію, паспрабуйце яшчэ раз ці зьвяжыцеся з support@mattermost.com." + }, + { + "id": "api.license.request_renewal_link.app_error", + "translation": "Памылка атрымання спасылкі абнаўлення ліцэнзіі" + }, + { + "id": "api.license.request-trial.can-start-trial.not-allowed", + "translation": "Тэрмін дзеяння гэтага выпрабавальнага ліцэнзійнага ключа для Mattermost Enterprise Edition скончыўся і больш не дзейнічае. Калі вы хочаце падоўжыць пробны перыяд, калі ласка, [звяжыцеся з нашым аддзелам продажаў] (https://mattermost.com/contact-us/)." + }, + { + "id": "api.license.request-trial.can-start-trial.error", + "translation": "Не ўдалося праверыць, ці можна запусціць пробную версію" + }, + { + "id": "api.license.request-trial.bad-request.terms-not-accepted", + "translation": "Вы павінны прыняць Пагадненне аб ацэнцы праграмнага забеспячэння Mattermost і Палітыку прыватнасці, каб запытаць ліцэнзію." + }, + { + "id": "api.license.request-trial.bad-request", + "translation": "Лік запытаных карыстачоў няправільна." + }, + { + "id": "api.license.remove_expired_license.failed.error", + "translation": "Не ўдалося адправіць паведамленне на электронную пошту аб дэактывацыі ліцэнзіі." + }, + { + "id": "api.license.client.old_format.app_error", + "translation": "Новы фармат карыстацкай ліцэнзіі пакуль не падтрымліваецца. Калі ласка, укажыце format=old у радку запыту." + }, + { + "id": "api.license.add_license.unique_users.app_error", + "translation": "Гэтая ліцэнзія падтрымлівае {{.Users}} карыстальнікаў, у той час як у вашай сістэме {{.Count}} унікальных карыстальнікаў. Унікальныя карыстальнікі падлічваюцца відавочна па адрасе электроннай пошты. Вы можаце ўбачыць агульную колькасць карыстальнікаў у пункце Справаздачы сайта -> Прагляд статыстыкі." + }, + { + "id": "api.license.add_license.save_active.app_error", + "translation": "Ідэнтыфікатар актыўнай ліцэнзіі не захаваны." + }, + { + "id": "api.license.add_license.save.app_error", + "translation": "Ліцэнзія не захавана." + }, + { + "id": "api.license.add_license.open.app_error", + "translation": "Не ўдалося адкрыць файл ліцэнзіі." + }, + { + "id": "api.license.add_license.no_file.app_error", + "translation": "Няма файла 'license' у запыце." + }, + { + "id": "api.license.add_license.invalid_count.app_error", + "translation": "Не ўдалося палічыць агульную колькасць унікальных карыстальнікаў." + }, + { + "id": "api.license.add_license.expired.app_error", + "translation": "Ліцэнзія скончылася ці яшчэ не пачалася." + }, + { + "id": "api.license.add_license.array.app_error", + "translation": "Пусты масіў 'license' у запыце." + }, + { + "id": "api.ldap_groups.license_error", + "translation": "ваша ліцэнзія не падтрымлівае ldap групы" + }, + { + "id": "api.ldap_groups.existing_user_name_error", + "translation": "імя групы ўжо існуе як імя карыстальніка" + }, + { + "id": "api.ldap_groups.existing_reserved_name_error", + "translation": "імя гурта ўжо існуе як зарэзерваванае імя" + }, + { + "id": "api.ldap_groups.existing_group_name_error", + "translation": "імя групы ўжо існуе" + }, + { + "id": "api.ldap_group.not_found", + "translation": "група LDAP не знойдзена" + }, + { + "id": "api.job.unable_to_download_job.incorrect_job_type", + "translation": "Тып задання, якое вы спрабуеце загрузіць, на дадзены момант не падтрымліваецца" + }, + { + "id": "api.job.unable_to_download_job", + "translation": "Немагчыма спампаваць гэтае заданне" + }, + { + "id": "api.job.unable_to_create_job.incorrect_job_type", + "translation": "Тып задання, якое вы спрабуеце стварыць, некарэктны" + }, + { + "id": "api.job.retrieve.nopermissions", + "translation": "Тыпы заданняў, якія вы спрабуеце атрымаць, не змяшчаюць дазволаў" + }, + { + "id": "api.invalid_redirect_url", + "translation": "Некарэктны url перанакіраванні" + }, + { + "id": "api.invalid_custom_url_scheme", + "translation": "Некарэктная карыстацкая схема url" + }, + { + "id": "app.channel.remove_all_deactivated_members.app_error", + "translation": "Мы не можам выдаліць дэактываваных карыстальнікаў з канала." + }, + { + "id": "app.channel.post_update_channel_purpose_message.updated_to", + "translation": "%s абнавіў загаловак канала на: %s" + }, + { + "id": "app.channel.post_update_channel_purpose_message.updated_from", + "translation": "%s абнавіў загаловак канала з: %s на: %s" + }, + { + "id": "app.channel.post_update_channel_purpose_message.retrieve_user.error", + "translation": "Не атрымалася атрымаць карыстальніка пры спробе абнаўлення паведамлення загалоўка канала %v" + }, + { + "id": "app.channel.post_update_channel_purpose_message.removed", + "translation": "%s выдаліў загаловак канала (быў: %s)" + }, + { + "id": "app.channel.post_update_channel_purpose_message.post.error", + "translation": "Не атрымалася абнавіць загаловак канала" + }, + { + "id": "app.channel.pinned_posts.app_error", + "translation": "Не ўдалося знайсці прымацаваныя паведамленні." + }, + { + "id": "app.channel.permanent_delete_members_by_user.app_error", + "translation": "Немагчыма выдаліць удзельніка канала." + }, + { + "id": "app.channel.permanent_delete.app_error", + "translation": "Не атрымалася выдаліць канал." + }, + { + "id": "app.channel.move_channel.members_do_not_match.error", + "translation": "Немагчыма перамясціць канал, калі ўсе яго ўдзельнікі не з'яўляюцца ўдзельнікамі Каманды." + }, + { + "id": "app.channel.migrate_channel_members.select.app_error", + "translation": "Не ўдалося зрабіць выбарку ўдзельнікаў каналаў." + }, + { + "id": "app.channel.get_unread.app_error", + "translation": "Не ўдалося атрымаць спіс непрачытаных паведамленняў у канале." + }, + { + "id": "app.channel.get_public_channels.get.app_error", + "translation": "Не ўдалося атрымаць публічныя каналы." + }, + { + "id": "app.channel.get_private_channels.get.app_error", + "translation": "Немагчыма атрымаць усе каналы." + }, + { + "id": "app.channel.get_pinnedpost_count.app_error", + "translation": "Не ўдалося атрымаць колькасць прымацаваных паведамленняў." + }, + { + "id": "app.channel.get_more_channels.get.app_error", + "translation": "Немагчыма атрымаць каналы." + }, + { + "id": "app.channel.get_members_by_ids.app_error", + "translation": "Не ўдалося атрымаць удзельнікаў канала." + }, + { + "id": "app.channel.get_members.app_error", + "translation": "Не ўдалося атрымаць удзельнікаў канала." + }, + { + "id": "app.channel.get_member_count.app_error", + "translation": "Не ўдалося атрымаць колькасць удзельнікаў канала." + }, + { + "id": "app.channel.get_member.missing.app_error", + "translation": "Не знойдзена ніводнага ўдзельніка канала па дадзеным ідэнтыфікатару карыстальніка і ідэнтыфікатару канала." + }, + { + "id": "app.channel.get_member.app_error", + "translation": "Не ўдалося атрымаць удзельніка канала." + }, + { + "id": "app.channel.get_for_post.app_error", + "translation": "Не ўдалося атрымаць канал для дадзенага паведамлення." + }, + { + "id": "app.channel.get_deleted.missing.app_error", + "translation": "Не існуе выдаленых каналаў." + }, + { + "id": "app.channel.get_deleted.existing.app_error", + "translation": "Не ўдалося знайсці існуючы выдалены канал." + }, + { + "id": "app.channel.get_channels_by_ids.get.app_error", + "translation": "Немагчыма атрымаць каналы." + }, + { + "id": "app.channel.get_channels_by_ids.app_error", + "translation": "Немагчыма атрымаць каналы па ідэнтыфікатарах." + }, + { + "id": "app.channel.get_channels_batch_for_indexing.get.app_error", + "translation": "Немагчыма атрымаць пакет каналаў для індэксацыі." + }, + { + "id": "app.channel.get_channels.not_found.app_error", + "translation": "Каналы не знойдзены." + }, + { + "id": "app.channel.get_channels.get.app_error", + "translation": "Немагчыма атрымаць каналы." + }, + { + "id": "app.channel.get_channel_counts.get.app_error", + "translation": "Немагчыма атрымаць колькасць каналаў." + }, + { + "id": "app.channel.get_by_scheme.app_error", + "translation": "Немагчыма атрымаць каналы для прадстаўленай схемы." + }, + { + "id": "app.channel.get_by_name.missing.app_error", + "translation": "Канал не існуе." + }, + { + "id": "app.channel.get_by_name.existing.app_error", + "translation": "Не ўдалося знайсці існуючы канал." + }, + { + "id": "app.channel.get_all_direct.app_error", + "translation": "Немагчыма атрымаць усе каналы асабістых паведамленняў." + }, + { + "id": "app.channel.get_all_channels_count.app_error", + "translation": "Не ўдалося падлічыць усе каналы." + }, + { + "id": "app.channel.get_all_channels.app_error", + "translation": "Немагчыма атрымаць усе каналы." + }, + { + "id": "app.channel.get_all.app_error", + "translation": "Немагчыма атрымаць усе каналы." + }, + { + "id": "app.channel.get.find.app_error", + "translation": "Узнікла праблема пры пошуку канала." + }, + { + "id": "app.channel.get.existing.app_error", + "translation": "Не ўдалося знайсці існуючы канал." + }, + { + "id": "app.channel.delete.app_error", + "translation": "Не атрымалася выдаліць канал." + }, + { + "id": "app.channel.create_initial_sidebar_categories.internal_error", + "translation": "Немагчыма стварыць пачатковыя катэгорыі бакавой панэлі для карыстальніка." + }, + { + "id": "app.channel.create_direct_channel.internal_error", + "translation": "Не ўдалося захаваць канал асабістых паведамленняў." + }, + { + "id": "app.channel.create_channel.no_team_id.app_error", + "translation": "Для стварэння канала неабходна ўказаць ідэнтыфікатар каманды." + }, + { + "id": "app.channel.create_channel.internal_error", + "translation": "Не ўдалося захаваць канал." + }, + { + "id": "app.channel.count_posts_since.app_error", + "translation": "Немагчыма падлічыць паведамленні з пазначанай даты." + }, + { + "id": "app.channel.clear_all_custom_role_assignments.select.app_error", + "translation": "Не ўдалося атрымаць удзельнікаў канала." + }, + { + "id": "app.channel.autofollow.app_error", + "translation": "Не ўдалося абнавіць удзел у абмеркаванні для ўказанага карыстальніка" + }, + { + "id": "app.channel.analytics_type_count.app_error", + "translation": "Не ўдалося атрымаць колькасць тыпаў каналаў." + }, + { + "id": "app.bot.permenent_delete.bad_id", + "translation": "Не атрымалася выдаліць робата." + }, + { + "id": "app.bot.permanent_delete.internal_error", + "translation": "Не атрымалася выдаліць робата назаўжды." + }, + { + "id": "app.bot.patchbot.internal_error", + "translation": "Не ўдалося абнавіць бота." + }, + { + "id": "api.invalid_channel", + "translation": "Канал, указаны ў запыце, не належыць карыстальніку" + }, + { + "id": "api.incoming_webhook.disabled.app_error", + "translation": "Уваходныя вэбхукі адключаныя сістэмным адміністратарам." + }, + { + "id": "api.image.get.app_error", + "translation": "Запытаны URL-адрас выявы не можа быць разабраны." + }, + { + "id": "api.getThreadsForUser.bad_params", + "translation": "Параметры Before і After з'яўляюцца ўзаемавыключальнымі з getThreadsForUser" + }, + { + "id": "api.file.write_file.app_error", + "translation": "Немагчыма запісаць файл." + }, + { + "id": "api.file.upload_file.too_large_detailed.app_error", + "translation": "Не атрымалася загрузіць файл {{.Filename}}. {{.Length}} байт перавышае максімальна дазволены ліміт {{.Limit}} байт." + }, + { + "id": "api.file.upload_file.storage.app_error", + "translation": "Немагчыма загрузіць файл. Сховішча малюнкаў не наладжана." + }, + { + "id": "api.file.upload_file.read_request.app_error", + "translation": "Немагчыма загрузіць файл(ы). Памылка чытання ці апрацоўкі дадзеных запыта." + }, + { + "id": "api.file.upload_file.read_form_value.app_error", + "translation": "Не атрымалася загрузіць файл(ы). Памылка ў значэнні {{.Formname}}." + }, + { + "id": "api.file.upload_file.multiple_channel_ids.app_error", + "translation": "Не атрымалася загрузіць файл(ы). Некалькі канфліктуючых channel_ids." + }, + { + "id": "api.file.upload_file.large_image_detailed.app_error", + "translation": "Памер файла {{.Filename}} ({{.Width}} на {{.Height}} пікселяў) перавышае ліміты." + }, + { + "id": "api.file.upload_file.large_image.app_error", + "translation": "Памер файла перавышае максімальныя памеры і не можа быць загружаны: {{.Filename}}" + }, + { + "id": "api.file.upload_file.incorrect_number_of_client_ids.app_error", + "translation": "Не атрымалася загрузіць файл(ы). Ёсць {{.NumClientIds}} client_id для {{.NumFiles}} файлаў." + }, + { + "id": "api.file.upload_file.incorrect_channelId.app_error", + "translation": "Немагчыма загрузіць файл. Няправільны ідэнтыфікатар канала: {{.channelId}}" + }, + { + "id": "api.file.test_connection_s3_bucket_does_not_exist.app_error", + "translation": "Пераканайцеся ў даступнасці кошыка Amazon S3 і праверце дазволы на доступ." + }, + { + "id": "api.file.test_connection_s3_auth.app_error", + "translation": "Немагчыма падлучыцца да S3. Праверце параметры аўтарызацыі і наладкі аўтэнтыфікацыі падлучэння Amazon S3." + }, + { + "id": "api.file.test_connection.app_error", + "translation": "Няма доступу да файлавага сховішча." + }, + { + "id": "api.file.remove_file.app_error", + "translation": "Немагчыма выдаліць файл." + }, + { + "id": "api.file.remove_directory.app_error", + "translation": "Немагчыма выдаліць каталог." + }, + { + "id": "api.file.read_file.reading_local.app_error", + "translation": "Адбылася памылка пры чытанні з лакальнага сервернага сховішча." + }, + { + "id": "api.file.read_file.app_error", + "translation": "Немагчыма прачытаць файл." + }, + { + "id": "api.file.no_driver.app_error", + "translation": "Не абраны файл драйвера." + }, + { + "id": "api.file.move_file.app_error", + "translation": "Немагчыма перанесці файл." + }, + { + "id": "api.file.list_directory.app_error", + "translation": "Немагчыма адлюстраваць каталог." + }, + { + "id": "api.file.get_public_link.no_post.app_error", + "translation": "Не ўдалося атрымаць публічную спасылку для файла. Файл павінен быць прымацаваны да паведамлення, якое можа быць прачытана бягучым карыстальнікам." + }, + { + "id": "api.file.get_public_link.disabled.app_error", + "translation": "Публічныя спасылкі адключаныя." + }, + { + "id": "api.file.get_file_thumbnail.no_thumbnail.app_error", + "translation": "Файл не мае мініяцюры." + }, + { + "id": "api.file.get_file_preview.no_preview.app_error", + "translation": "Файл не змяшчае выявы папярэдняга прагляду." + }, + { + "id": "api.file.get_file.public_invalid.app_error", + "translation": "Няправільная публічная спасылка." + }, + { + "id": "api.file.file_size.app_error", + "translation": "Немагчыма атрымаць памер файла." + }, + { + "id": "api.file.file_reader.app_error", + "translation": "Немагчыма атрымаць аб'ект чытання файлаў." + }, + { + "id": "api.file.file_mod_time.app_error", + "translation": "Немагчыма атрымаць час апошняй змены файла." + }, + { + "id": "api.file.file_exists.app_error", + "translation": "Немагчыма праверыць існаванне файла." + }, + { + "id": "api.file.attachments.disabled.app_error", + "translation": "Укладанні адключаныя на гэтым серверы." + }, + { + "id": "api.file.append_file.app_error", + "translation": "Немагчыма дадаць дадзеныя да файла." + }, + { + "id": "api.export.export_not_found.app_error", + "translation": "Немагчыма знайсці файл экспарту." + }, + { + "id": "api.error_set_first_admin_visit_marketplace_status", + "translation": "Памылка пры спробе захаваць статус першага наведвання крамы адміністратарам у краме." + }, + { + "id": "api.error_get_first_admin_visit_marketplace_status", + "translation": "Памылка пры спробе атрымаць з крамы статус першага наведвання крамы адміністратарам." + }, + { + "id": "api.emoji.upload.open.app_error", + "translation": "Немагчыма стварыць эмоджы. Адбылася памылка пры спробе адкрыць прымацаваную выяву." + }, + { + "id": "api.emoji.upload.large_image.too_large.app_error", + "translation": "Немагчыма стварыць эмоджы. Выява павінна быць менш {{.MaxWidth}} х {{.MaxHeight}}." + }, + { + "id": "api.emoji.upload.large_image.gif_encode_error", + "translation": "Немагчыма стварыць эмоджы. Адбылася памылка пры кадаванні GIF выявы." + }, + { + "id": "api.emoji.upload.large_image.gif_decode_error", + "translation": "Немагчыма стварыць эмоджы. Адбылася памылка пры дэкадаванні GIF выявы." + }, + { + "id": "api.emoji.upload.large_image.encode_error", + "translation": "Немагчыма стварыць эмоджы. Адбылася памылка пры кадаванні выявы." + }, + { + "id": "api.emoji.upload.large_image.decode_error", + "translation": "Немагчыма стварыць эмоджы. Адбылася памылка пры дэкадаванні выявы." + }, + { + "id": "api.emoji.upload.image.app_error", + "translation": "Немагчыма стварыць эмоджы. Павінен быць PNG, JPEG ці GIF файл." + }, + { + "id": "api.emoji.storage.app_error", + "translation": "Сховішча файлаў не наладжана. Калі ласка, наладзьце S3 або лакальнае сервернае файлавае сховішча." + }, + { + "id": "api.emoji.get_image.read.app_error", + "translation": "Не атрымалася прачытаць файл выявы для эмоджы." + }, + { + "id": "api.emoji.get_image.decode.app_error", + "translation": "Не атрымалася дэкадаваць файл выявы для эмоджы." + }, + { + "id": "api.emoji.disabled.app_error", + "translation": "Карыстальніцкія смайлы былі адключаныя сістэмным адміністратарам." + }, + { + "id": "api.emoji.create.too_large.app_error", + "translation": "Немагчыма стварыць эмоджы. Выява павінна быць не больш за 1 MB." + }, + { + "id": "api.emoji.create.parse.app_error", + "translation": "Немагчыма стварыць эмоджы. Незразумелы запыт." + }, + { + "id": "api.emoji.create.internal_error", + "translation": "server_error: Адбылася ўнутраная памылка сервера пры стварэнні эмоджы." + }, + { + "id": "api.emoji.create.duplicate.app_error", + "translation": "Не ўдалося стварыць эмоджы. Ужо існуе іншы эмоджы з такім жа імем." + }, + { + "id": "api.email_batching.send_batched_email_notification.title", + "translation": "У вас ёсць новыя паведамленні" + }, + { + "id": "api.email_batching.send_batched_email_notification.subject", + "translation": { + "few": "", + "many": "", + "one": "[{{.SiteName}}] Новае апавяшчэнне за {{.Day}} {{.Month}}, {{.Year}}" + } + }, + { + "id": "api.command_join.fail.app_error", + "translation": "Пры падключэнні да канала адбылася памылка." + }, + { + "id": "api.command_join.desc", + "translation": "Далучыцца да адкрытага канала" + }, + { + "id": "api.command_invite_people.permission.app_error", + "translation": "У вас няма правоў для запрашэння новых удзельнікаў." + }, + { + "id": "api.command_invite.user_not_in_team.app_error", + "translation": "@{{.Username}} не з'яўляецца ўдзельнікам каманды." + }, + { + "id": "api.command_invite.user_already_in_channel.app_error", + "translation": "{{.User}} ужо знаходзіцца ў канале." + }, + { + "id": "api.command_invite.success", + "translation": "{{.User}} дададзены ў канал {{.Channel}}." + }, + { + "id": "api.command_invite.private_channel.app_error", + "translation": "Немагчыма знайсці канал {{.Channel}}. Калі ласка, выкарыстоўвайце спіс каналаў для ідэнтыфікацыі." + }, + { + "id": "api.command_invite.permission.app_error", + "translation": "У вас недастаткова правоў для дадання {{.User}} у {{.Channel}}." + }, + { + "id": "api.command_invite.missing_user.app_error", + "translation": "Мы не змаглі знайсці карыстальніка. Верагодна, ён заблакаваны Сістэмным адміністратарам." + }, + { + "id": "api.command_invite.missing_message.app_error", + "translation": "Адсутнічае імя карыстальніка і канал." + }, + { + "id": "api.command_invite.group_constrained_user_denied", + "translation": "Гэты канал кіруецца групамі. Гэты карыстач не з'яўляецца часткай групы, якая сінхранізуецца з гэтым каналам." + }, + { + "id": "api.command_invite.fail.app_error", + "translation": "Пры падключэнні да канала адбылася памылка." + }, + { + "id": "api.command_invite.directchannel.app_error", + "translation": "Вы не можаце дадаць кагосьці ў канал асабістых паведамленняў." + }, + { + "id": "api.command_invite.desc", + "translation": "Запрасіць карыстальніка ў канал" + }, + { + "id": "api.command_invite.channel.error", + "translation": "Не ўдалося знайсці канал {{.Channel}}. Для ідэнтыфікацыі каналаў выкарыстоўвайце [дэскрыптар канала](https://docs.mattermost.com/channels/channel-naming-conventions.html)." + }, + { + "id": "api.command_invite.channel.app_error", + "translation": "Памылка атрымання бягучага канала." + }, + { + "id": "api.command_help.desc", + "translation": "Адкрыць старонку дапамогі Mattermost" + }, + { + "id": "api.command_groupmsg.permission.app_error", + "translation": "У вас няма адпаведных дазволаў для стварэння новага групавога паведамлення." + }, + { + "id": "api.command_groupmsg.min_users.app_error", + "translation": "Мінімальная колькасць атрымальнікаў групавых паведамленняў абмежавана {{.MinUsers}} карыстальнікамі." + }, + { + "id": "api.command_groupmsg.max_users.app_error", + "translation": "Максімальная колькасць атрымальнікаў групавых паведамленняў абмежавана {{.MaxUsers}} карыстальнікамі." + }, + { + "id": "api.command_groupmsg.invalid_user.app_error", + "translation": { + "few": "", + "many": "", + "one": "Не ўдалося знайсці карыстальніка: {{.Users}}" + } + }, + { + "id": "api.command_groupmsg.group_fail.app_error", + "translation": "Пры стварэнні групавога паведамлення адбылася памылка." + }, + { + "id": "api.command_groupmsg.fail.app_error", + "translation": "Адбылася памылка пры перадачы паведамлення карыстальнікам." + }, + { + "id": "api.command_groupmsg.desc", + "translation": "Адправіць групавое паведамленне ўказаным карыстальнікам" + }, + { + "id": "api.command_expand_collapse.fail.app_error", + "translation": "Адбылася памылка пры адкрыцці перадпряглядаў." + }, + { + "id": "api.command_expand.success", + "translation": "Цяпер спасылкі на выявы паказваюцца цалкам" + }, + { + "id": "api.command_expand.desc", + "translation": "Адключыць аўтаматычнае згортванне малюнкаў перадпрагляду" + }, + { + "id": "api.command_echo.message.app_error", + "translation": "Перад паведамленьнем павінна быць каманда /echo." + }, + { + "id": "api.command_echo.hint", + "translation": "'паведамленне' [затрымка ў секундах]" + }, + { + "id": "api.command_echo.high_volume.app_error", + "translation": "Вялікі аб'ём рэха-запыту, немагчыма апрацаваць запыт." + }, + { + "id": "api.command_echo.desc", + "translation": "Вывесці тэкст ад імя вашага ўліковага запісу" + }, + { + "id": "api.command_echo.delay.app_error", + "translation": "Затрымкі павінны быць менш за 10000 секунд." + }, + { + "id": "api.command_dnd.success", + "translation": "Рэжым \"Не турбаваць\" уключаны. Вы не будзеце атрымліваць абвесткі на працоўным стале або мабільных прыладах да таго часу, пакуль не адключыце гэты рэжым." + }, + { + "id": "api.command_dnd.desc", + "translation": "Рэжым \"Не турбаваць\" адключае настольныя і мабільныя push-паведамлення." + }, + { + "id": "api.command_custom_status.success", + "translation": "Ваш статус устаноўлены ў \"{{.EmojiName}} {{.StatusMessage}}\". Вы можаце змяніць свой статус з усплывальнага акна ў загалоўку бакавой панэлі канала." + }, + { + "id": "api.command_custom_status.hint", + "translation": "[:emoji_name:] [status_message] або скінуць" + }, + { + "id": "api.command_custom_status.desc", + "translation": "Устанавіць або скінуць ваш статус" + }, + { + "id": "api.command_custom_status.clear.success", + "translation": "Ваш статус быў скінуты." + }, + { + "id": "api.command_custom_status.clear.app_error", + "translation": "Памылка ачысткі статусу." + }, + { + "id": "api.command_custom_status.app_error", + "translation": "Памылка наладкі статусу." + }, + { + "id": "api.command_collapse.success", + "translation": "Цяпер спасылкі на выявы сгортваюцца" + }, + { + "id": "api.command_collapse.desc", + "translation": "Уключыць згортванне перадпрагляда малюнкаў" + }, + { + "id": "api.command_code.message.app_error", + "translation": "Перад паведамленьнем павінна быць каманда /code." + }, + { + "id": "api.command_code.desc", + "translation": "Паказвае тэкст як блок кода" + }, + { + "id": "api.command_channel_rename.update_channel.app_error", + "translation": "Памылка абнаўлення бягучага канала." + }, + { + "id": "api.command_channel_rename.too_short.app_error", + "translation": "Назва канала павінна быць не карацейшая за {{.Length}} сімвалаў." + }, + { + "id": "api.command_channel_rename.too_long.app_error", + "translation": "Назва канала павінна быць не даўжэй {{.Length}} сімвалаў." + }, + { + "id": "api.command_channel_rename.permission.app_error", + "translation": "У вас няма правоў на перайменаванне канала." + }, + { + "id": "api.command_channel_rename.message.app_error", + "translation": "Перад паведамленьнем павінна быць каманда /rename." + }, + { + "id": "api.command_channel_rename.direct_group.app_error", + "translation": "Немагчыма перайменаваць канал асабістых паведамленняў." + }, + { + "id": "api.command_channel_rename.channel.app_error", + "translation": "Памылка атрымання бягучага канала." + }, + { + "id": "api.command_channel_remove.channel.app_error", + "translation": "Памылка атрымання бягучага канала." + }, + { + "id": "api.command_channel_purpose.update_channel.max_length", + "translation": "Уведзены тэкст перавышае ліміт сімвалаў. Прызначэнне канала абмежавана {{.MaxLength}} сімваламі." + }, + { + "id": "api.command_channel_purpose.update_channel.app_error", + "translation": "Памылка абнаўлення прызначэння канала." + }, + { + "id": "api.command_channel_purpose.permission.app_error", + "translation": "У вас няма правоў для рэдагавання прызначэння канала." + }, + { + "id": "api.command_channel_purpose.message.app_error", + "translation": "Перад паведамленьнем павінна быць каманда /purpose." + }, + { + "id": "api.command_channel_purpose.direct_group.app_error", + "translation": "Немагчыма ўсталяваць прызначэнне для каналаў асабістых паведамленняў. Выкарыстоўвайце /header, каб замест гэтага ўсталяваць загаловак." + }, + { + "id": "api.command_channel_purpose.desc", + "translation": "Рэдагаваць прызначэнне канала" + }, + { + "id": "api.command_channel_purpose.channel.app_error", + "translation": "Памылка атрымання бягучага канала." + }, + { + "id": "api.command_channel_header.update_channel.max_length", + "translation": "Уведзены тэкст перавышае ліміт сімвалаў. Загаловак канала абмежаваны {{.MaxLength}} сімваламі." + }, + { + "id": "api.command_channel_header.update_channel.app_error", + "translation": "Памылка абнаўлення загалоўка канала." + }, + { + "id": "api.command_channel_header.permission.app_error", + "translation": "У вас няма правоў для рэдагавання загалоўка канала." + }, + { + "id": "api.command_channel_header.message.app_error", + "translation": "Перад паведамленьнем павінна быць каманда /header." + }, + { + "id": "api.command_channel_header.desc", + "translation": "Рэдагаваць загаловак канала" + }, + { + "id": "api.command_channel_header.channel.app_error", + "translation": "Памылка атрымання бягучага канала." + }, + { + "id": "api.command_away.success", + "translation": "Цяпер ваш стан \"Адышоў\"" + }, + { + "id": "api.command_away.desc", + "translation": "Устанавіць стан \"Адышоў\"" + }, + { + "id": "api.command.team_mismatch.app_error", + "translation": "Немагчыма абнавіць каманды(commands) сярод каманд(teams)." + }, + { + "id": "api.command.invite_people.no_email", + "translation": "Калі ласка, укажыце адзін або некалькі дзейных адрасоў электроннай пошты" + }, + { + "id": "api.command.invite_people.invite_off", + "translation": "На гэтым серверы адключана стварэнне карыстальнікаў, запрашэнне(я) не адпраўлена" + }, + { + "id": "api.command.invite_people.fail", + "translation": "Узнікла памылка пры адпраўцы запрашэння па электроннай пошце" + }, + { + "id": "api.command.invite_people.email_off", + "translation": "Электронная пошта не настроена, запрашэнні не адпраўлены" + }, + { + "id": "api.command.invite_people.email_invitations_off", + "translation": "Запрашэнні па электроннай пошце адключаныя, запрашэнне не адпраўлена" + }, + { + "id": "api.command.invite_people.desc", + "translation": "Адправіць ліст з запрашэннем у вашу каманду Mattermost" + }, + { + "id": "api.command.execute_command.start.app_error", + "translation": "Не знойдзены трыгер каманды." + }, + { + "id": "api.command.execute_command.not_found.app_error", + "translation": "Каманда з трыгерам '{{.Trigger}}' не знойдзена. Каб адправіць паведамленне, якое пачынаецца з \"/\", паспрабуйце дадаць прабел ў пачатку паведамлення." + }, + { + "id": "api.command.execute_command.format.app_error", + "translation": "У слове-трыгеры каманды адсутнічае пачатковы сімвал слэш" + }, + { + "id": "api.command.execute_command.failed_resp.app_error", + "translation": "Каманда з трыгерам '{{.Trigger}}' вярнула адказ {{.Status}}." + }, + { + "id": "api.command.execute_command.failed_empty.app_error", + "translation": "Каманда з трыгерам '{{.Trigger}}' вярнула пусты адказ." + }, + { + "id": "api.command.execute_command.failed.app_error", + "translation": "Каманда з трыгерам '{{.Trigger}}' завяршылася з памылкай." + }, + { + "id": "api.command.execute_command.create_post_failed.app_error", + "translation": "Каманда '{{.Trigger}}' не адправіла адказ. Калі ласка, звярніцеся да вашага сістэмнага адміністратара." + }, + { + "id": "api.command.duplicate_trigger.app_error", + "translation": "Гэтае трыгер слова ўжо выкарыстоўваецца. Калі ласка, абярыце іншае." + }, + { + "id": "api.command.disabled.app_error", + "translation": "Каманды былі адключаныя сістэмным адміністратарам." + }, + { + "id": "api.command.command_post.forbidden.app_error", + "translation": "Указаны карыстальнік не з'яўляецца ўдзельнікам названага канала." + }, + { + "id": "api.command.admin_only.app_error", + "translation": "Інтэграцыі могуць быць выкананы толькі адміністратарамі." + }, + { + "id": "api.cloud.request_error", + "translation": "Памылка апрацоўкі запыту да CWS." + }, + { + "id": "api.cloud.license_error", + "translation": "Ваша ліцэнзія не падтрымлівае воблачныя запыты." + }, + { + "id": "api.cloud.cws_webhook_event_missing_error", + "translation": "Падзея вэбхука не была апрацавана. Або яна адсутнічае, або некарэктная." + }, + { + "id": "api.cloud.app_error", + "translation": "Унутраная памылка падчас запыту воблачнага API." + }, + { + "id": "api.channel.update_team_member_roles.scheme_role.app_error", + "translation": "Абраная роля кіруецца схемай і не можа быць ужытая наўпрост да ўдзельніка каманды." + }, + { + "id": "api.channel.update_team_member_roles.changing_guest_role.app_error", + "translation": "Недапушчальнае абнаўленне ўдзельніка каманды: вы не можаце дадаваць ці выдаляць гасцявую ролю ўручную." + }, + { + "id": "api.channel.update_channel_scheme.scheme_scope.error", + "translation": "Немагчыма прымяніць схему да канала, бо дадзеная схема не з'яўляецца схемай канала." + }, + { + "id": "api.channel.update_channel_scheme.license.error", + "translation": "Ваша ліцэнзія не падтрымлівае змену схемы канала" + }, + { + "id": "api.channel.update_channel_privacy.default_channel_error", + "translation": "Канал па змаўчанні нельга зрабіць прыватным." + }, + { + "id": "api.channel.update_channel_member_roles.scheme_role.app_error", + "translation": "Абраная роля кіруецца схемай і не можа быць ужытая наўпрост да ўдзельніка канала." + }, + { + "id": "api.channel.update_channel_member_roles.guest_and_user.app_error", + "translation": "Недапушчальнае абнаўленне ўдзельніка канала: карыстальнік павінен быць госцем ці карыстальнікам, але не абодвума." + }, + { + "id": "api.channel.update_channel_member_roles.changing_guest_role.app_error", + "translation": "Недапушчальнае абнаўленне ўдзельніка канала: Вы не можаце дадаваць ці выдаляць гасцявую ролю ўручную." + }, + { + "id": "api.channel.update_channel.typechange.app_error", + "translation": "Тып канала не можа быць абноўлены." + }, + { + "id": "api.channel.update_channel.tried.app_error", + "translation": "Спроба выканання недапушчальнага абнаўлення канала па змаўчанні {{.Channel}}." + }, + { + "id": "api.channel.update_channel.deleted.app_error", + "translation": "Дадзены канал быў перамешчаны ў архіў, альбо выдалены." + }, + { + "id": "api.channel.restore_channel.unarchived", + "translation": "{{.Username}} разархіваваў канал." + }, + { + "id": "api.channel.restore_channel.restored.app_error", + "translation": "Немагчыма разархіваваць канал. Ён не архіваваны." + }, + { + "id": "api.channel.rename_channel.cant_rename_group_messages.app_error", + "translation": "Вы не можаце перайменаваць канал групавых паведамленняў." + }, + { + "id": "api.channel.rename_channel.cant_rename_direct_messages.app_error", + "translation": "Вы не можаце перайменаваць канал асабістых паведамленняў." + }, + { + "id": "api.channel.remove_user_from_channel.app_error", + "translation": "Немагчыма выдаліць карыстальніка з гэтага тыпу канала." + }, + { + "id": "api.channel.remove_members.denied", + "translation": "Выдаленне з канала забаронена наступным карыстальнікам з-за абмежаванняў групы: {{.UserIDs}}" + }, + { + "id": "api.channel.remove_member.removed", + "translation": "%v выдалены з канала." + }, + { + "id": "api.channel.remove_member.group_constrained.app_error", + "translation": "Немагчыма выдаліць карыстальніка з канала ў сувязі з абмежаваннем групы." + }, + { + "id": "api.channel.remove_channel_member.type.app_error", + "translation": "Не атрымалася выдаліць карыстальніка з канала." + }, + { + "id": "api.channel.remove.default.app_error", + "translation": "Немагчыма выдаліць карыстальніка з канала па змаўчанні {{.Channel}}." + }, + { + "id": "api.channel.post_user_add_remove_message_and_forget.error", + "translation": "Не атрымалася адправіць паведамленне аб далучэнні/пакіданні" + }, + { + "id": "api.channel.post_update_channel_header_message_and_forget.updated_to", + "translation": "%s абнавіў загаловак канала на: %s" + }, + { + "id": "api.channel.post_update_channel_header_message_and_forget.updated_from", + "translation": "%s абнавіў загаловак канала з: %s на: %s" + }, + { + "id": "api.channel.post_update_channel_header_message_and_forget.retrieve_user.error", + "translation": "Не атрымалася атрымаць карыстальніка пры спробе абнаўлення загалоўка канала" + }, + { + "id": "api.channel.post_update_channel_header_message_and_forget.removed", + "translation": "%s выдаліў загаловак канала (быў: %s)" + }, + { + "id": "api.channel.post_update_channel_header_message_and_forget.post.error", + "translation": "Не атрымалася апублікаваць абнаўлення загалоўка канала" + }, + { + "id": "api.channel.post_update_channel_displayname_message_and_forget.updated_from", + "translation": "%s абнавіў імя канала з: %s на: %s" + }, + { + "id": "api.channel.post_update_channel_displayname_message_and_forget.retrieve_user.error", + "translation": "Не атрымалася атрымаць карыстальніка пры спробе абнаўлення імя канала" + }, + { + "id": "api.channel.post_update_channel_displayname_message_and_forget.create_post.error", + "translation": "Ну атрымалася адправіць паведамленне аб абнаўленні бачнага імя канала" + }, + { + "id": "api.channel.post_channel_privacy_message.error", + "translation": "Немагчыма адправіць паведамленне абнаўлення прыватнасці канала." + }, + { + "id": "api.channel.patch_update_channel.forbidden.app_error", + "translation": "Не ўдалося абнавіць канал." + }, + { + "id": "api.channel.patch_channel_moderations.license.error", + "translation": "Ваша ліцэнзія не падтрымлівае мадэрацыю канала" + }, + { + "id": "api.channel.move_channel.type.invalid", + "translation": "Немагчыма перамясціць каналы асабістых паведамленняў ці групавыя каналы." + }, + { + "id": "api.channel.leave.left", + "translation": "%v пакінуў канал." + }, + { + "id": "api.channel.leave.last_member.app_error", + "translation": "Вы адзіны ўдзельнік, паспрабуйце выдаліць прыватны канал замест таго, каб пакінуць." + }, + { + "id": "api.channel.leave.direct.app_error", + "translation": "Немагчыма пакінуць канал асабістых паведамленняў." + }, + { + "id": "api.channel.leave.default.app_error", + "translation": "Немагчыма пакінуць канал па змаўчанні {{.Channel}}." + }, + { + "id": "api.channel.join_channel.post_and_forget", + "translation": "%v далучыўся да канала." + }, + { + "id": "api.channel.join_channel.permissions.app_error", + "translation": "У вас няма адпаведных правоў." + }, + { + "id": "api.channel.guest_join_channel.post_and_forget", + "translation": "%v далучыўся да канала як госць." + }, + { + "id": "api.channel.get_channel_moderations.license.error", + "translation": "Ваша ліцэнзія не падтрымлівае мадэрацыю канала" + }, + { + "id": "api.channel.delete_channel.type.invalid", + "translation": "Немагчыма выдаліць прамыя ці групавыя каналы паведамленняў" + }, + { + "id": "api.channel.delete_channel.deleted.app_error", + "translation": "Дадзены канал быў перамешчаны ў архіў, альбо выдалены." + }, + { + "id": "api.channel.delete_channel.cannot.app_error", + "translation": "Немагчыма выдаліць канал па змаўчанні {{.Channel}}." + }, + { + "id": "api.channel.delete_channel.archived", + "translation": "%v архіваваў канал." + }, + { + "id": "api.channel.create_group.bad_user.app_error", + "translation": "Адзін з прапанаваных карыстальнікаў не існуе." + }, + { + "id": "api.channel.create_group.bad_size.app_error", + "translation": "Канал групы карыстальнікаў павінен змяшчаць не менш за 3 і не больш за 8 карыстальнікаў." + }, + { + "id": "api.channel.create_direct_channel.invalid_user.app_error", + "translation": "Няправільны ідэнтыфікатар карыстальніка пры стварэнні канала асабістых паведамленняў." + }, + { + "id": "api.channel.create_channel.max_channel_limit.app_error", + "translation": "Нельга стварыць больш за {{.MaxChannelsPerTeam}} каналаў для гэтай каманды." + }, + { + "id": "api.channel.create_channel.direct_channel.team_restricted_error", + "translation": "Паміж гэтымі карыстальнікамі немагчыма стварыць прамы канал, бо ў іх няма агульнай каманды." + }, + { + "id": "api.channel.create_channel.direct_channel.app_error", + "translation": "Трэба выкарыстоўваць createDirectChannel з API для стварэння канала асабістых паведамленняў." + }, + { + "id": "api.channel.channel_member_counts_by_group.license.error", + "translation": "Ваша ліцэнзія не падтрымлівае групы" + }, + { + "id": "api.channel.change_channel_privacy.public_to_private", + "translation": "Канал пераўтвораны ў прыватны." + }, + { + "id": "api.channel.change_channel_privacy.private_to_public", + "translation": "Канал пераўтвораны ў публічны і да яго можа далучыцца любы ўдзельнік." + }, + { + "id": "api.channel.add_user_to_channel.type.app_error", + "translation": "Не магу дадаць карыстальніка ў канал гэтага тыпу." + }, + { + "id": "api.channel.add_user.to.channel.failed.deleted.app_error", + "translation": "Немагчыма дадаць карыстальніка да канала, бо ён быў выдалены з каманды." + }, + { + "id": "api.channel.add_user.to.channel.failed.app_error", + "translation": "Не ўдалося дадаць карыстальніка ў канал." + }, + { + "id": "api.channel.add_members.user_denied", + "translation": "Удзел у канале забаронены наступным карыстальнікам з-за абмежаванняў групы: {{ .UserIDs }}" + }, + { + "id": "api.channel.add_members.error", + "translation": "Памылка пры дабаўленні ўдзельніка(ў) каналаў." + }, + { + "id": "api.channel.add_member.added", + "translation": "%v дададзены(а) у канал карыстальнікам %v." + }, + { + "id": "api.channel.add_guest.added", + "translation": "%v дададзены(а) ў канал як госць карыстальнікам %v." + }, + { + "id": "api.bot.teams_channels.add_message_mobile", + "translation": "Калі ласка, дадайце мяне ў каманды і каналы, з якімі мне трэба ўзаемадзейнічаць. Гэта можна зрабіць у браўзэры або ў дадатку Mattermost для ПК." + }, + { + "id": "api.bot.create_disabled", + "translation": "Стварэнне робатаў было адключана." + }, + { + "id": "api.admin.upload_brand_image.too_large.app_error", + "translation": "Немагчыма загрузіць файл. Ён занадта вялікі." + }, + { + "id": "api.admin.upload_brand_image.storage.app_error", + "translation": "Немагчыма загрузіць выяву. Сховішча выяў не наладжана." + }, + { + "id": "api.admin.upload_brand_image.parse.app_error", + "translation": "Не ўдалося разабраць складовую форму." + }, + { + "id": "api.admin.upload_brand_image.no_file.app_error", + "translation": "Няма файла 'image' у запыце." + }, + { + "id": "api.admin.upload_brand_image.array.app_error", + "translation": "Пусты масіў 'image' у запыце." + }, + { + "id": "api.admin.test_s3.missing_s3_bucket", + "translation": "Патрабуецца S3 Bucket" + }, + { + "id": "api.admin.test_email.subject", + "translation": "Mattermost - Тэставанне настроек электроннай пошты" + }, + { + "id": "api.templates.deactivate_body.title", + "translation": "Ваш рахунак быў дэактываваны на {{.ServerURL}}" + }, + { + "id": "api.templates.deactivate_body.info", + "translation": "Вы адключылі свой уліковы запіс на {{.SiteURL}}." + }, + { + "id": "api.templates.copyright", + "translation": "© 2021 Mattermost, Inc. 530 Lytton Avenue, Second floor, Palo Alto, CA, 94301" + }, + { + "id": "api.templates.cloud_welcome_email.title", + "translation": "Ваша 14-дзённая пробная версія {{.WorkSpace}} гатова да працы!" + }, + { + "id": "api.templates.cloud_welcome_email.subtitle_info", + "translation": "Выканайце наступныя крокі, каб пабудаваць свае каманды і атрымаць максімальную аддачу ад сваёй працоўнай прасторы." + }, + { + "id": "api.templates.cloud_welcome_email.subtitle", + "translation": "Наладзьце вашу працоўную прастору" + }, + { + "id": "api.templates.cloud_welcome_email.start_questions", + "translation": "У вас ёсць пытанні аб тым, як пачаць? Напішыце нам па адрасе" + }, + { + "id": "api.templates.cloud_welcome_email.signin_sub_info2", + "translation": "для зручнейшай працы на Windows, Linux Mac, iOS і Android." + }, + { + "id": "api.templates.cloud_welcome_email.signin_sub_info", + "translation": "Увайдзіце ў сваю працоўную прастору на нашым" + }, + { + "id": "api.templates.cloud_welcome_email.mm_apps", + "translation": "мабільныя і настольныя прыкладанні" + }, + { + "id": "api.templates.cloud_welcome_email.invite_sub_info", + "translation": "Падзяліцеся гэтай спасылкай, каб запрасіць новых удзельнікаў далучыцца ў {{.WorkSpace}}:" + }, + { + "id": "api.templates.cloud_welcome_email.invite_info", + "translation": "Запрасіць у працоўную прастору" + }, + { + "id": "api.templates.cloud_welcome_email.info2", + "translation": "Абавязкова захавайце спасылку ці дадайце яе ў закладкі для выкарыстання ў будучыні." + }, + { + "id": "api.templates.cloud_welcome_email.download_mm_info", + "translation": "Спампаваць Mattermost App" + }, + { + "id": "api.templates.cloud_welcome_email.add_apps_sub_info", + "translation": "Аптымізуе сваю працу з такімі інструментамі, як Github, Jira and Zoom. Праглядзіце ўсе інтэграцыі, якія ёсць у нас у нашым" + }, + { + "id": "api.templates.cloud_welcome_email.add_apps_info", + "translation": "Дадаць прыкладання ў вашу працоўную прастору" + }, + { + "id": "api.team.update_team_scheme.scheme_scope.error", + "translation": "Немагчыма ўсталяваць схему каманды бо дадзеная схема не з'яўляецца схемай каманды." + }, + { + "id": "api.team.update_team_scheme.license.error", + "translation": "Ваша ліцэнзія не падтрымлівае змену схемы каманды" + }, + { + "id": "api.team.update_team_member_roles.guest_and_user.app_error", + "translation": "Недапушчальнае абнаўленне ўдзельніка каманды: карыстальнік павінен быць госцем ці карыстальнікам, але не абодвума." + }, + { + "id": "api.team.update_restricted_domains.mismatch.app_error", + "translation": "Абмежаванні Каманды на дамене {{.Domain}} забаронены ў наладах сістэмы. Калі ласка, звярніцеся да сістэмнага адміністратара." + }, + { + "id": "api.team.update_member_roles.not_a_member", + "translation": "Указаны карыстальнік не з'яўляецца ўдзельнікам названай групы." + }, + { + "id": "api.team.team_icon.update.app_error", + "translation": "Адбылася памылка пры абнаўленні значка каманды." + }, + { + "id": "api.team.set_team_icon.write_file.app_error", + "translation": "Не ўдалося захаваць значок каманды." + }, + { + "id": "api.team.set_team_icon.too_large.app_error", + "translation": "Немагчыма загрузіць выяву. Файл занадта вялікі." + }, + { + "id": "api.team.set_team_icon.storage.app_error", + "translation": "Не ўдалося загрузіць файл. Сховішча малюнкаў не наладжана." + }, + { + "id": "api.team.set_team_icon.parse.app_error", + "translation": "Не ўдалося разабраць складовую форму." + }, + { + "id": "api.team.set_team_icon.open.app_error", + "translation": "Не атрымалася адкрыць файл выявы." + }, + { + "id": "api.team.set_team_icon.no_file.app_error", + "translation": "Няма файла 'image' у запыце." + }, + { + "id": "api.team.set_team_icon.get_team.app_error", + "translation": "Адбылася памылка пры атрыманні Каманды." + }, + { + "id": "api.team.set_team_icon.encode.app_error", + "translation": "Не ўдалося закадзіраваць значок каманды." + }, + { + "id": "api.team.set_team_icon.decode.app_error", + "translation": "Не ўдалося дэкадаваць значок каманды." + }, + { + "id": "api.team.set_team_icon.check_image_limits.app_error", + "translation": "Памылка праверкі абмежаванняў выявы. Дазвол занадта высокае." + }, + { + "id": "api.team.set_team_icon.array.app_error", + "translation": "Пусты масіў 'image' у запыце." + }, + { + "id": "api.team.search_teams.pagination_not_implemented.public_team_search", + "translation": "Пагінацыя не выконваецца для пошуку ў публічных групах." + }, + { + "id": "api.team.search_teams.pagination_not_implemented.private_team_search", + "translation": "Пагінацыя не выконваецца для пошуку ў прыватных групах." + }, + { + "id": "api.team.remove_user_from_team.removed", + "translation": "%v выдалены з каманды." + }, + { + "id": "api.team.remove_user_from_team.missing.app_error", + "translation": "Карыстальнік неактыўны для ўдзелу ў гэтай камандзе." + }, + { + "id": "api.team.remove_team_icon.get_team.app_error", + "translation": "Адбылася памылка пры атрыманні Каманды." + }, + { + "id": "api.team.remove_member.group_constrained.app_error", + "translation": "Немагчыма выдаліць карыстальніка з кіраванай групамі каманды." + }, + { + "id": "api.team.move_channel.success", + "translation": "Канал перайшоў у гэтую каманду з %v." + }, + { + "id": "api.team.move_channel.post.error", + "translation": "Не атрымалася апублікаваць паведамленне перасоўвання канала." + }, + { + "id": "api.team.leave.left", + "translation": "%v пакінуў(ла) каманду." + }, + { + "id": "api.team.join_user_to_team.allowed_domains.app_error", + "translation": "E-mail павінен належаць рэальнаму дамену (напрыклад @example.com). Калі ласка, звярніцеся да сістэмнага адміністратара па дадатковую інфармацыю." + }, + { + "id": "api.team.join_team.post_and_forget", + "translation": "%v далучыўся да каманды." + }, + { + "id": "api.team.is_team_creation_allowed.domain.app_error", + "translation": "E-mail павінен належаць рэальнаму дамену (напрыклад @example.com). Калі ласка, звярніцеся да сістэмнага адміністратара па дадатковую інфармацыю." + }, + { + "id": "api.team.is_team_creation_allowed.disabled.app_error", + "translation": "Магчымасць стварэння каманд была адключаная. Калі ласка, звярніцеся да вашага сістэмнага адміністратара па падрабязнасці." + }, + { + "id": "api.team.invite_members.no_one.app_error", + "translation": "Няма нікога для запрашэння." + }, + { + "id": "api.team.invite_members.invalid_email.app_error", + "translation": "Гэты адрас паштовай скрыні не належыць спісу дазволеных даменаў: {{.Addresses}}. Калі ласка, звярніцеся да Вашага сістэмнага адміністратара па інфармацыю." + }, + { + "id": "api.team.invite_members.disabled.app_error", + "translation": "Запрашэнні па электроннай пошце адключаныя." + }, + { + "id": "api.team.invite_guests_to_channels.invalid_body.app_error", + "translation": "Цела запыту некарэктнае ці адсутнічае." + }, + { + "id": "api.team.invite_guests.channel_in_invalid_team.app_error", + "translation": "Каналы ў запрашэнні павінны быць часткай каманды ў запрашэнні." + }, + { + "id": "api.team.invalidate_all_email_invites.app_error", + "translation": "Памылка анулявання e-mail запрашэнняў." + }, + { + "id": "api.team.import_team.unavailable.app_error", + "translation": "Няправільны запыт: адсутнічае поле \"filesize\"." + }, + { + "id": "api.team.import_team.parse.app_error", + "translation": "Не ўдалося разабраць складовую форму." + }, + { + "id": "api.team.import_team.open.app_error", + "translation": "Не магу адкрыць файл." + }, + { + "id": "api.team.import_team.no_import_from.app_error", + "translation": "Няправільны запыт: адсутнічае поле \"importFrom\"." + }, + { + "id": "api.team.import_team.no_file.app_error", + "translation": "Няма файла 'file' у запыце." + }, + { + "id": "api.team.import_team.integer.app_error", + "translation": "Памер файла не з'яўляецца цэлым." + }, + { + "id": "api.team.import_team.array.app_error", + "translation": "Пусты масіў 'file' у запыце." + }, + { + "id": "api.team.get_team_icon.read_file.app_error", + "translation": "Немагчыма прачытаць файл значка каманды." + }, + { + "id": "api.team.get_team_icon.filesettings_no_driver.app_error", + "translation": "Няправільнае імя драйвера ў наладах файлаў. Павінна быць 'local' ці 'amazons3'." + }, + { + "id": "api.team.get_invite_info.not_open_team", + "translation": "Запрашэнне недапушчальна, бо каманда не з'яўляецца адкрытай." + }, + { + "id": "api.team.get_all_teams.insufficient_permissions", + "translation": "У вас няма правоў для атрымання спісу ўсіх каманд" + }, + { + "id": "api.team.demote_user_to_guest.license.error", + "translation": "Ваша ліцэнзія не падтрымлівае гасцявыя ўліковыя запісы" + }, + { + "id": "api.team.demote_user_to_guest.disabled.error", + "translation": "Гасцявыя ўліковыя запісы адключаныя." + }, + { + "id": "api.team.add_user_to_team_from_invite.guest.app_error", + "translation": "Гасцям абмежавана магчымасць далучацца да каманды праз спасылку-запрашэнне. Запытайце гасцёўня запрашэнне па электроннай пошце для далучэння да каманды." + }, + { + "id": "api.team.add_user_to_team.missing_parameter.app_error", + "translation": "Для дадання карыстальніка ў каманду патрабуецца пазначыць параметр." + }, + { + "id": "api.team.add_user_to_team.added", + "translation": "%v дададзены(а) у каманду карыстальнікам %v." + }, + { + "id": "api.team.add_team_member.invalid_body.app_error", + "translation": "Немагчыма прааналізаваць цела запыту." + }, + { + "id": "api.team.add_members.user_denied", + "translation": "Гэтая каманда кіруецца групамі. Гэты карыстач не з'яўляецца часткай групы, якая сінхранізуецца з гэтай камандай." + }, + { + "id": "api.team.add_members.error", + "translation": "Памылка пры даданні ўдзельніка каманды." + }, + { + "id": "api.system.update_viewed_notices.failed", + "translation": "Абнаўленне прагледжаных апавяшчэнняў не ўдалося" + }, + { + "id": "api.system.update_notices.validating_failed", + "translation": "Не ўдалося праверыць умовы апавяшчэння аб прадукце" + }, + { + "id": "api.system.update_notices.parse_failed", + "translation": "Не ўдалося атрымаць апавяшчэння аб прадукце" + }, + { + "id": "api.system.update_notices.fetch_failed", + "translation": "Не ўдалося атрымаць апавяшчэння аб прадукце" + }, + { + "id": "api.system.update_notices.clear_failed", + "translation": "Не атрымалася выдаліць старыя апавяшчэнні аб прадукце" + }, + { + "id": "api.system.id_loaded.not_available.app_error", + "translation": "Push-паведамлення з усталяваным ідэнтыфікатарам (ID Loaded) не сканфігураваны ці не падтрымліваюцца на гэтым серверы." + }, + { + "id": "api.slackimport.slack_import.zip.file_too_large", + "translation": "{{.Filename}} у zip архіве занадта вялікі для апрацоўкі імпарцёрам з Slack\n" + }, + { + "id": "api.slackimport.slack_import.zip.app_error", + "translation": "Немагчыма адкрыць zip-архіў якія экспартуюцца з Slack дадзеных.\n" + }, + { + "id": "api.slackimport.slack_import.team_fail", + "translation": "Не ўдалося атрымаць каманду, у якую неабходна імпартаваць.\n" + }, + { + "id": "api.slackimport.slack_import.open.app_error", + "translation": "Немагчыма адкрыць файл: {{.Filename}}.\n" + }, + { + "id": "api.slackimport.slack_import.note3", + "translation": "- Дадатковыя памылкі могуць быць знойдзены ў часопісах сервера.\n" + }, + { + "id": "api.slackimport.slack_import.note2", + "translation": "- Паведамленні ботаў Slack на дадзены момант не падтрымліваюцца.\n" + }, + { + "id": "api.slackimport.slack_import.note1", + "translation": "- Некаторыя паведамленні маглі быць не імпартаваныя, паколькі не падтрымліваюцца гэтым іпарцёрам.\n" + }, + { + "id": "api.slackimport.slack_import.log", + "translation": "Лог імпарту Mattermost Slack\n" + }, + { + "id": "api.slackimport.slack_add_users.unable_import", + "translation": "Не ўдалося імпартаваць карыстальніка: {{.Username}}.\n" + }, + { + "id": "api.slackimport.slack_add_users.missing_email_address", + "translation": "Карыстальнік {{.Username}} не мае адрасы электроннай пошты ў экспарце з Slack. Выкарыстоўваецца {{.Email}} як заглушка. Карыстальнік павінен абнавіць адрас электроннай пошты пры ўваходзе ў сістэму.\n" + }, + { + "id": "api.slackimport.slack_add_users.merge_existing_failed", + "translation": "Карыстальнік Slack аб'яднаны з існуючым карыстальнікам Mattermost па супадзенні email {{.Email}} і імя карыстальніка {{.Username}}, але было немагчыма дадаць карыстальніка ў яго каманду.\n" + }, + { + "id": "api.slackimport.slack_add_users.merge_existing", + "translation": "Slack карыстальнік зліўся з існуючым карыстальнікам Mattermost з адпаведным адрасам электроннай пошты {{.Email}} і імем карыстальніка {{.Username}}.\n" + }, + { + "id": "api.slackimport.slack_add_users.email_pwd", + "translation": "Slack карыстальнік з email {{.Email}} і паролем {{.Password}} імпартаваны.\n" + }, + { + "id": "api.slackimport.slack_add_channels.merge", + "translation": "Slack канал {{.DisplayName}} ужо існуе ў выглядзе канала Mattermost. Выраблена зліццё.\n" + }, + { + "id": "api.slackimport.slack_add_channels.import_failed", + "translation": "Не ўдалося імпартаваць Slack канал {{.DisplayName}}.\n" + }, + { + "id": "api.slackimport.slack_add_channels.failed_to_add_user", + "translation": "Не атрымалася дадаць карыстальніка Slack {{.Username}} у канал.\n" + }, + { + "id": "api.slackimport.slack_add_bot_user.unable_import", + "translation": "Не ўдалося імпартаваць карыстальніка Integration / Slack Bot {{.Username}}.\n" + }, + { + "id": "api.slackimport.slack_add_bot_user.email_pwd", + "translation": "Slack карыстальнік з email {{.Email}} і паролем {{.Password}} імпартаваны.\n" + }, + { + "id": "api.server.warn_metric.number_of_teams_5.start_trial_notification_success.message", + "translation": "Ваша пробная версія Enterprise зараз актыўная. Перайдзіце ў **Сістэмная кансоль > Упраўленне карыстальнікамі > Правы доступу**, каб уключыць дадатковыя правы доступу." + }, + { + "id": "api.server.warn_metric.number_of_teams_5.start_trial.notification_body", + "translation": "У вашай сістэме Mattermost зараз ёсць некалькі каманд. У многіх каманд ёсць свой упадабаны спосаб каардынацыі і супрацоўніцтва, у тым ліку тое, як ствараюцца каналы, хто можа запрашаць новых удзельнікаў каманды і як ажыццяўляецца інтэграцыя. Схемы групавога кіравання дазваляюць вам настройваць правы карыстальнікаў у кожнай камандзе ў адпаведнасці з іх канкрэтнымі патрэбамі.\n\n[Больш падрабязна аб выкарыстанні пашыраных правоў] (https://www.mattermost.com/docs-advanced-permissions-team-override/?utm_medium=product&utm_source=mattermost-advisor-bot&utm_content=advanced-permissions-team-override).\n\nНаціснуўшы кнопку \"Пачаць выкарыстоўваць пробную версію\", я згаджаюся з [Пагадненнем аб ацэнцы праграмнага забеспячэння Mattermost](https://mattermost.com/software-evaluation-agreement/), [Палітыкай прыватнасці](https://mattermost.com/ privacy-policy/) і атрымліваю паведамленні аб прадукце па email." + }, + { + "id": "api.server.warn_metric.number_of_teams_5.notification_body", + "translation": "У вашай сістэме Mattermost зараз ёсць некалькі каманд. У шматлікіх каманд ёсць свой упадабаны спосаб каардынацыі і супрацоўніцтва, у тым ліку тое, як ствараюцца каналы, хто можа запрашаць новых удзельнікаў каманды і як ажыццяўляецца інтэграцыя. Схемы групавога кіравання дазваляюць вам настройваць правы карыстальнікаў у кожнай камандзе ў адпаведнасці з іх канкрэтнымі патрэбамі.\n\n[Больш падрабязна аб выкарыстанні пашыраных правоў] (https://www.mattermost.com/docs-advanced-permissions-team-override/?utm_medium=product&utm_source=mattermost-advisor-bot&utm_content=advanced-permissions-team-override).\n\nНаціснуўшы кнопку \"Звязацца з намі\", вы падзяліцеся сваёй інфармацыяй з кампаніяй Mattermost, Inc. [Больш падрабязна](https://mattermost.com/pl/default-admin-advisory)" + }, + { + "id": "api.server.warn_metric.number_of_teams_5.contact_us.email_body", + "translation": "Запыт кантакту з Mattermost. Мне цікава даведацца больш аб пашыраных правах са схемамі для каманд.\n" + }, + { + "id": "api.server.warn_metric.number_of_posts_2M.start_trial.notification_success.message", + "translation": "Ваша пробная версія Enterprise зараз актыўная. Калі ў вас ёсць сервер Elasticsearch, перайдзіце ў **Сістэмная кансоль > Серада > Elasticsearch** для наладкі Elasticsearch." + }, + { + "id": "api.server.warn_metric.number_of_posts_2M.start_trial.notification_body", + "translation": "У вашай сістэме Mattermost ёсць вялікая колькасць паведамленняў. Пошук у базе дадзеных Mattermost па змаўчанні пачынае паказваць зніжэнне прадукцыйнасці прыкладна пры 2,5 мільёнах паведамленняў. Калі паведамленняў больш за 5 мільёнаў, Elasticsearch можа дапамагчы пазбегнуць значных праблем з прадукцыйнасцю, такіх як таймаўты пры пошуку і апрацоўцы згадак. Звяжыцеся з намі, каб пазнаць больш і дайце ведаць, чым мы можам дапамагчы.\n\n[Больш падрабязна аб паляпшэнні прадукцыйнасці] (https://www.mattermost.com/docs-elasticsearch/?utm_medium=product&utm_source=mattermost-advisor-bot&utm_content=elasticsearch).\n\nНаціснуўшы кнопку \"Пачаць выкарыстоўваць пробную версію\", я згаджаюся з [Пагадненнем аб ацэнцы праграмнага забеспячэння Mattermost](https://mattermost.com/software-evaluation-agreement/), [Палітыкай прыватнасці](https://mattermost.com/ privacy-policy/) і атрымліваю паведамленні аб прадукце па email." + }, + { + "id": "api.server.warn_metric.number_of_posts_2M.notification_body", + "translation": "У вашай сістэме Mattermost ёсць вялікая колькасць паведамленняў. Пошук у базе дадзеных Mattermost па змаўчанні пачынае паказваць зніжэнне прадукцыйнасці прыкладна пры 2,5 мільёнах паведамленняў. Калі паведамленняў больш за 5 мільёнаў, Elasticsearch можа дапамагчы пазбегнуць значных праблем з прадукцыйнасцю, такіх як таймаўты пры пошуку і апрацоўцы згадак. Звяжыцеся з намі, каб пазнаць больш і дайце ведаць, чым мы можам дапамагчы.\n\n[Больш падрабязна аб паляпшэнні прадукцыйнасці] (https://www.mattermost.com/docs-elasticsearch/?utm_medium=product&utm_source=mattermost-advisor-bot&utm_content=elasticsearch).\n\nНаціснуўшы кнопку \"Звязацца з намі\", вы падзяліцеся сваёй інфармацыяй з кампаніяй Mattermost, Inc. [Больш падрабязна] (https://mattermost.com/pl/default-admin-advisory)" + }, + { + "id": "api.server.warn_metric.number_of_posts_2M.contact_us.email_body", + "translation": "Запыт кантакту з Mattermost. Мне цікава даведацца больш аб паляпшэнні прадукцыйнасці з дапамогай Elasticsearch.\n" + }, + { + "id": "api.server.warn_metric.number_of_channels_50.start_trial.notification_success.message", + "translation": "Ваша пробная версія Enterprise зараз актыўная. Перайдзіце ў **Сістэмная кансоль > Упраўленне карыстальнікамі > Правы доступу**, каб уключыць дадатковыя правы доступу." + }, + { + "id": "api.admin.test_email.reenter_password", + "translation": "SMTP сервер, порт ці імя карыстальніка зменены. Калі ласка, паўторна ўвядзіце пароль SMTP для праверкі злучэння." + }, + { + "id": "api.admin.test_email.missing_server", + "translation": "Патрабуецца SMTP сервер" + }, + { + "id": "api.admin.test_email.body", + "translation": "Здаецца, ваша электронная пошта Mattermost наладжаная правільна!" + }, + { + "id": "api.admin.saml.not_available.app_error", + "translation": "SAML 2.0 не наладжаны ці не падтрымліваецца на гэтым серверы." + }, + { + "id": "api.admin.saml.metadata.app_error", + "translation": "Адбылася памылка пры пабудове метададзеных пастаўшчыка паслуг." + }, + { + "id": "api.admin.saml.invalid_xml_missing_ssoservices.app_error", + "translation": "Адсутнічае вузел паслуг SSO пастаўшчыка паслугі ідэнтыфікацыі ў XML." + }, + { + "id": "api.admin.saml.invalid_xml_missing_keydescriptor.app_error", + "translation": "Адсутнічае вузел дэскрыптараў ключоў пастаўшчыка паслугі ідэнтыфікацыі ў XML." + }, + { + "id": "api.admin.saml.invalid_xml_missing_idpssodescriptors.app_error", + "translation": "Адсутнічае вузел дэскрыптараў SSO пастаўшчыка паслугі ідэнтыфікацыі ў XML." + }, + { + "id": "api.admin.saml.failure_reset_authdata_to_email.app_error", + "translation": "Не ўдалося скінуць поле AuthData на Email." + }, + { + "id": "api.admin.saml.failure_parse_idp_certificate.app_error", + "translation": "Выяўлена памылка пры разборы інфармацыі метададзеных, атрыманай ад пастаўшчыка паслугі ідэнтыфікацыі ў сертыфікат." + }, + { + "id": "api.admin.saml.failure_get_metadata_from_idp.app_error", + "translation": "Не ўдалося атрымаць метададзеныя з URL-адраса пастаўшчыка паслугі ідэнтыфікацыі." + }, + { + "id": "api.admin.remove_certificate.delete.app_error", + "translation": "Узнікла памылка пры выдаленні сертыфіката." + }, + { + "id": "api.admin.get_brand_image.storage.app_error", + "translation": "Сховішча выяў не наладжана." + }, + { + "id": "api.admin.delete_brand_image.storage.not_found", + "translation": "Немагчыма выдаліць выяву брэнда, не знойдзена." + }, + { + "id": "api.admin.add_certificate.no_file.app_error", + "translation": "Няма файла пад атрыбутам 'certificate' у запыце." + }, + { + "id": "api.admin.add_certificate.array.app_error", + "translation": "Няма файла пад атрыбутам 'certificate' у запыце." + }, + { + "id": "api.email_batching.send_batched_email_notification.subTitle", + "translation": "Глядзіце ніжэй зводку Вашых новых паведамленняў." + }, + { + "id": "api.email_batching.add_notification_email_to_batch.disabled.app_error", + "translation": "Паштовыя аб'яднанні адключаныя сістэмным адміністратарам." + }, + { + "id": "api.email_batching.add_notification_email_to_batch.channel_full.app_error", + "translation": "Канал-атрымальнік паштовага аб'яднання быў поўны. Калі ласка, павялічце значэнне EmailBatchingBufferSize." + }, + { + "id": "api.email.send_warn_metric_ack.missing_server.app_error", + "translation": "Патрабуецца SMTP сервер" + }, + { + "id": "api.email.send_warn_metric_ack.invalid_warn_metric.app_error", + "translation": "Немагчыма знайсці метрыку папяраджання." + }, + { + "id": "api.email.send_warn_metric_ack.failure.app_error", + "translation": "Не ўдалося адправіць адміністратару ліст з пацверджаннем" + }, + { + "id": "api.custom_status.recent_custom_statuses.delete.app_error", + "translation": "Не ўдалося выдаліць апошні статус. Паспрабуйце спачатку дадаць статус або зьвяжыцеся з адміністратарам для атрымання падрабязнай інфармацыі." + }, + { + "id": "api.custom_status.disabled", + "translation": "Функцыя карыстацкага статута адключаная. Калі ласка, звяжыцеся з адміністратарам для больш падрабязнай інфармацыі." + }, + { + "id": "api.create_terms_of_service.empty_text.app_error", + "translation": "Увядзіце тэкст для сваіх умоў абслугоўвання." + }, + { + "id": "api.create_terms_of_service.custom_terms_of_service_disabled.app_error", + "translation": "Карыстальніцкія ўмовы абслугоўвання адключаныя." + }, + { + "id": "api.context.token_provided.app_error", + "translation": "Сесія не OAuth, але токен прысутнічае ў радку запыту." + }, + { + "id": "api.context.session_expired.app_error", + "translation": "Няверная ці скончаная сесія, калі ласка, увайдзіце зноў." + }, + { + "id": "api.context.server_busy.app_error", + "translation": "Сервер заняты, некрытычныя службы часова недаступныя." + }, + { + "id": "api.context.remote_id_missing.app_error", + "translation": "Адсутнічае ID бяспечнага злучэння." + }, + { + "id": "api.context.remote_id_mismatch.app_error", + "translation": "Неадпаведнасць ID бяспечнага злучэння." + }, + { + "id": "api.context.remote_id_invalid.app_error", + "translation": "Немагчыма знайсці бяспечны ID злучэння {{.RemoteId}}." + }, + { + "id": "api.context.permissions.app_error", + "translation": "У вас няма адпаведных правоў." + }, + { + "id": "api.context.mfa_required.app_error", + "translation": "На гэтым серверы патрабуецца шматфактарная аўтарызацыя." + }, + { + "id": "api.context.local_origin_required.app_error", + "translation": "Гэтая канчатковая кропка патрабуе лакальнай крыніцы запыту." + }, + { + "id": "api.context.invalid_url_param.app_error", + "translation": "Няправільны ці прапушчаны параметр {{.Name}} у URL запыта." + }, + { + "id": "api.context.invalid_token.error", + "translation": "Няправільны токен сесіі={{.Token}}, err={{.Error}}" + }, + { + "id": "api.context.invalid_body_param.app_error", + "translation": "Няправільна або прапушчана {{.Name}} у целе запыту." + }, + { + "id": "api.context.get_user.app_error", + "translation": "Немагчыма атрымаць карыстальніка з UserID сеансу." + }, + { + "id": "api.context.404.app_error", + "translation": "Прабачце, мы не змаглі знайсці старонку." + }, + { + "id": "api.config.update_config.restricted_merge.app_error", + "translation": "Памылка аб'яднання з дадзенай канфігурацыяй." + }, + { + "id": "api.config.update_config.not_allowed_security.app_error", + "translation": "Змена {{.Name}} не дазволена па меркаваннях бяспекі." + }, + { + "id": "api.config.update_config.diff.app_error", + "translation": "Не ўдалося правесці параўнанне канфігурацый" + }, + { + "id": "api.config.update_config.clear_siteurl.app_error", + "translation": "URL сайта не можа быць ачышчаны." + }, + { + "id": "api.config.reload_config.app_error", + "translation": "Не атрымалася перазагрузіць канфігурацыю." + }, + { + "id": "api.config.patch_config.restricted_merge.app_error", + "translation": "Памылка аб'яднання з дадзенай канфігурацыяй." + }, + { + "id": "api.config.patch_config.diff.app_error", + "translation": "Не ўдалося правесці параўнанне канфігурацый" + }, + { + "id": "api.config.migrate_config.app_error", + "translation": "Не ўдалося перанесці сховішча канфігурацыі." + }, + { + "id": "api.config.get_config.restricted_merge.app_error", + "translation": "Памылка аб'яднання з дадзенай канфігурацыяй." + }, + { + "id": "api.config.client.old_format.app_error", + "translation": "Новы фармат канфігурацыі кліента пакуль не падтрымліваецца. Калі ласка, укажыце format=old у радку запыту." + }, + { + "id": "api.command_shrug.desc", + "translation": "Дадаць ¯\\_(ツ)_/¯ у ваша паведамленне" + }, + { + "id": "api.command_shortcuts.unsupported.app_error", + "translation": "Гарачыя клавішы недаступныя на прыладзе." + }, + { + "id": "api.command_shortcuts.desc", + "translation": "Адлюстроўвае спіс спалучэнняў клавіш" + }, + { + "id": "api.command_share.unshare_channel.help", + "translation": "Адмяніць уласцівасць \"сумесны\" для бягучага канала" + }, + { + "id": "api.command_share.unknown_action", + "translation": "Невядомае дзеянне `{{.Action}}`. Даступныя дзеянні: {{.Actions}}" + }, + { + "id": "api.command_share.uninvite_remote_id.help", + "translation": "Ідэнтыфікатар бяспечнага падлучэння для адмены запрашэння." + }, + { + "id": "api.command_share.uninvite_remote.help", + "translation": "Адмена запрашэння на бяспечнае злучэнне ад гэтага сумеснага канала" + }, + { + "id": "api.command_share.shared_channel_unshare.error", + "translation": "Не атрымалася адключыць абагульванне да гэтага канала: {{.Error}}." + }, + { + "id": "api.command_share.shared_channel_unavailable", + "translation": "Гэты канал больш не абагулены." + }, + { + "id": "api.command_share.share_read_only.hint", + "translation": "[readonly] - 'Y' ці 'N'. Па змаўчанні 'N'" + }, + { + "id": "api.command_share.share_read_only.help", + "translation": "Канал будзе абагулены ў рэжыме толькі для чытання" + }, + { + "id": "api.command_share.share_channel.error", + "translation": "Не атрымалася абагуліць гэты канал: {{.Error}}" + }, + { + "id": "api.command_share.service_disabled", + "translation": "Служба сумесных каналаў адключана." + }, + { + "id": "api.command_share.remote_uninvited", + "translation": "Адмненена запрашэнне для бяспечнаго злучэння `{{.RemoteId}}`." + }, + { + "id": "api.command_share.remote_table_header", + "translation": "| Бяспечнае злучэнне | URL сайта | Толькі чытанне | Запрашэнне прынята | Анлайн | Апошняя сінхранізацыя |" + }, + { + "id": "api.command_share.remote_not_valid", + "translation": "Для адмены запрашэння павінна быць указана сапраўднае бяспечнае злучэнне." + }, + { + "id": "api.command_share.remote_id_invalid.error", + "translation": "Ідэнтыфікатар бяспечнага злучэння некарэктны: {{.Error}}" + }, + { + "id": "api.command_share.remote_id.help", + "translation": "ID існуючага бяспечнага злучэння. Глядзіце Каманду `secure-connection`, каб дадаць бяспечнае злучэнне." + }, + { + "id": "api.command_share.remote_already_invited", + "translation": "Бяспечнае злучэнне ўжо запрошана." + }, + { + "id": "api.command_share.permission_required", + "translation": "Для кіравання агульнымі каналамі патрабуецца дазвол `{{.Permission}}`." + }, + { + "id": "api.command_share.not_shared_channel_unshare", + "translation": "Нельга адмяніць уласцівасць \"сумесны\" для канала, які не з'яўляецца сумесным." + }, + { + "id": "api.command_share.no_remote_invited", + "translation": "Ніякія бяспечныя злучэнні ў гэты канал не запрашаліся." + }, + { + "id": "api.command_share.must_specify_valid_remote", + "translation": "Павінен быць указаны сапраўдны ідэнтыфікатар бяспечнага злучэння для запрашэння." + }, + { + "id": "api.command_share.missing_action", + "translation": "Прапушчана дзеянне. Даступныя дзеянні: : {{.Actions}}" + }, + { + "id": "api.command_share.invite_remote.help", + "translation": "Запрашае вонкавы асобнік Mattermost ў бягучы сумесны канал" + }, + { + "id": "api.command_share.invitation_sent", + "translation": "Запрашэнне сумеснага канала адпраўлена `{{.Name}} {{.SiteURL}}`." + }, + { + "id": "api.command_share.invalid_value.error", + "translation": "Некарэктнае значэнне '{{.Arg}}': {{.Error}}" + }, + { + "id": "api.command_share.fetch_remote_status.error", + "translation": "Не атрымалася атрымаць статут для бяспечных злучэнняў: {{.Error}}." + }, + { + "id": "api.command_share.fetch_remote.error", + "translation": "Памылка пры атрыманні бяспечных злучэнняў: {{.Error}}" + }, + { + "id": "api.command_share.desc", + "translation": "Абагульвае бягучы канал з вонкавым серверам Mattermost." + }, + { + "id": "api.command_share.could_not_uninvite.error", + "translation": "Не атрымалася адмяніць запрашэнне `{{.RemoteId}}`: {{.Error}}" + }, + { + "id": "api.command_share.check_channel_exist.error", + "translation": "Памылка пры праверцы існавання сумеснага канала: {{.Error}}" + }, + { + "id": "api.command_share.channel_status_id", + "translation": "Статус для ідэнтыфікатара канала `{{.ChannelId}}`" + }, + { + "id": "api.command_share.channel_status.help", + "translation": "Адлюстроўвае статус для гэтага сумеснага канала" + }, + { + "id": "api.command_share.channel_shared", + "translation": "Гэты канал зараз абагулены." + }, + { + "id": "api.command_share.channel_remote_id_not_exists", + "translation": "Бяспечнае злучэнне агульнага канала `{{.RemoteId}}` не існуе для гэтага канала." + }, + { + "id": "api.command_share.channel_invite_not_home.error", + "translation": "Немагчыма запрасіць бяспечнае злучэнне на агульны канал, створаны недзе ў іншым месцы." + }, + { + "id": "api.command_share.channel_invite.error", + "translation": "Памылка запрашэння `{{.Name}}` у гэты канал: {{.Error}}" + }, + { + "id": "api.command_settings.unsupported.app_error", + "translation": "Налады праз каманду не даступныя на прыладзе." + }, + { + "id": "api.command_settings.desc", + "translation": "Адкрыць дыялог настроек" + }, + { + "id": "api.command_search.unsupported.app_error", + "translation": "Пошук недаступны на вашым прыладзе." + }, + { + "id": "api.command_search.desc", + "translation": "Шукаць тэкст у паведамленнях" + }, + { + "id": "api.command_remove.user_not_in_channel", + "translation": "{{.Username}} не з'яўляецца ўдзельнікам гэтага канала." + }, + { + "id": "api.command_remove.permission.app_error", + "translation": "У Вас няма правоў для выдалення ўдзельніка." + }, + { + "id": "api.command_remove.missing.app_error", + "translation": "Мы не змаглі знайсці карыстальніка. Верагодна, ён заблакаваны Сістэмным адміністратарам." + }, + { + "id": "api.command_remove.message.app_error", + "translation": "Паведамленне павінна быць прадстаўлена камандай /remove ці /kick." + }, + { + "id": "api.command_remove.group_constrained_user_denied", + "translation": "Карыстальнік не можа быць выдалены вамі з канала, паколькі ён з'яўляецца ўдзельнікам груп, злучаных з гэтым каналам. Каб выдаліць яго з гэтага канала, ён павінен быць выдалены з злучаных груп." + }, + { + "id": "api.command_remove.direct_group.app_error", + "translation": "Вы не можаце выдаліць каго-небудзь з канала асабістых паведамленняў." + }, + { + "id": "api.command_remove.desc", + "translation": "Выдаліць удзельніка з канала" + }, + { + "id": "api.command_remote.status.help", + "translation": "Адлюстроўвае статус для ўсіх бяспечных злучэнняў" + }, + { + "id": "api.command_remote.site_url_not_set", + "translation": "URL сайта не зададзены. Усталюйце яго ў Сістэмнай кансолі > Асяроддзе > Вэб-сервер." + }, + { + "id": "api.command_remote.service_not_enabled", + "translation": "Служба бяспечнага злучэння не ўключана." + }, + { + "id": "api.command_remote.service_disabled", + "translation": "Служба бяспечнага злучэння адключана." + }, + { + "id": "api.command_remote.remove_remote_id.help", + "translation": "Ідэнтыфікатар бяспечнага злучэння, якое выдаляецца." + }, + { + "id": "api.command_remote.remove_remote.error", + "translation": "Не атрымалася выдаліць бяспечнае злучэнне: {{.Error}}" + }, + { + "id": "api.command_remote.remove.help", + "translation": "Выдаляе бяспечнае злучэнне" + }, + { + "id": "api.command_remote.remotes_not_found", + "translation": "Бяспечных падлучэнняў не знойдзена." + }, + { + "id": "api.command_remote.remote_table_header", + "translation": "| Бяспечнае злучэнне | Адлюстроўванае імя | Ідэнтыфікатар падключэння | URL сайта | Запрашэнне прынята | Анлайн | Апошні пінг |" + }, + { + "id": "api.command_remote.remote_add_remove.help", + "translation": "Дадаць/выдаліць бяспечныя злучэнні. Даступныя дзеянні: {{.Actions}}" + }, + { + "id": "api.command_remote.permission_required", + "translation": "Для кіравання бяспечнымі злучэннямі патрабуецца дазвол `{{.Permission}}`." + }, + { + "id": "api.command_remote.name.hint", + "translation": "Унікальная назва для бяспечнага злучэння" + }, + { + "id": "api.command_remote.missing_empty", + "translation": "Адсутнічае ці пусты `{{.Arg}}`" + }, + { + "id": "api.command_remote.missing_command", + "translation": "Адсутнічае каманда. Даступныя дзеянні: {{.Actions}}" + }, + { + "id": "api.command_remote.invite_summary", + "translation": "Адпраўце наступнае зашыфраванае AES256-bit запрашэнне адміністратару знешняга сервера Mattermost разам з паролем. Для прыняцця запрашэння яны будуць выкарыстоўваць слэш-каманду `{{.Command}}`.\n\n```\n{{.Invitation}}\n```\n\n**Прадастаўце знешняму сайту шыфраванае злучэнне да вашага кластара праз** {{.SiteURL}}" + }, + { + "id": "api.command_remote.invite_password.hint", + "translation": "Пароль, які выкарыстоўваецца для шыфравання запрашэння" + }, + { + "id": "api.command_remote.invite.help", + "translation": "Стварыць бяспечнае злучэнне" + }, + { + "id": "api.command_remote.invitation.hint", + "translation": "Зашыфраванае запрашэнне ад абароненага злучэння" + }, + { + "id": "api.command_remote.invitation.help", + "translation": "Запрашэнне ад абароненага злучэння" + }, + { + "id": "api.command_remote.incorrect_password.error", + "translation": "Не атрымалася расшыфраваць запрашэнне. Няправільны пароль або сапсаванае запрашэнне: {{.Error}}" + }, + { + "id": "api.command_remote.fetch_status.error", + "translation": "Не атрымалася атрымаць бяспечныя злучэнні: {{.Error}}" + }, + { + "id": "api.command_remote.encrypt_invitation.error", + "translation": "Не атрымалася зашыфраваць запрашэнне: {{.Error}}" + }, + { + "id": "api.command_remote.displayname.hint", + "translation": "Як адлюстроўваецца назва бяспечнага злучэння" + }, + { + "id": "api.command_remote.displayname.help", + "translation": "Як адлюстроўваецца назва бяспечнага злучэння" + }, + { + "id": "api.command_remote.desc", + "translation": "Запрасіць бяспечныя злучэнні для сувязі паміж асобнікамі Mattermost." + }, + { + "id": "api.command_remote.decode_invitation.error", + "translation": "Не атрымалася дэкадаваць запрашэнне: {{.Error}}" + }, + { + "id": "api.command_remote.cluster_removed", + "translation": "Бяспечнае злучэнне {{.RemoteId}} {{.Result}}." + }, + { + "id": "api.command_remote.add_remote.error", + "translation": "Не атрымалася дадаць бяспечнае злучэнне: {{.Error}}" + }, + { + "id": "api.command_remote.accept_invitation.error", + "translation": "Не атрымалася прыняць запрашэнне: {{.Error}}" + }, + { + "id": "api.command_remote.accept_invitation", + "translation": "Запрашэнне прынята і пацверджана.\nSiteURL: {{.SiteURL}}" + }, + { + "id": "api.command_remote.accept.help", + "translation": "Прыняць запрашэнне ад знешняга сервера Mattermost" + }, + { + "id": "api.command_online.success", + "translation": "Вы зараз у сетцы" + }, + { + "id": "api.command_online.desc", + "translation": "Устанавіць стан \"У сетцы\"" + }, + { + "id": "api.command_offline.success", + "translation": "Вы зараз не ў сетцы" + }, + { + "id": "api.command_offline.desc", + "translation": "Устанавіць стан \"Не ў сетцы\"" + }, + { + "id": "api.command_mute.success_unmute_direct_msg", + "translation": "Паведамленні ў гэтым канале больш не адключаны." + }, + { + "id": "api.command_mute.success_unmute", + "translation": "Апавяшчэнні ў {{.Channel}} ўключаны." + }, + { + "id": "api.command_mute.success_mute_direct_msg", + "translation": "Вы не будзеце атрымліваць апавяшчэнні для гэтага канала да таго часу, пакуль адключаны гук." + }, + { + "id": "api.command_mute.success_mute", + "translation": "Вы не будзеце атрымліваць апавяшчэнні для {{.Channel}} да таго часу, пакуль адключаны апавяшчэнні." + }, + { + "id": "api.command_mute.not_member.error", + "translation": "Немагчыма адключыць канал {{.Channel}}, таму што вы не з'яўляецеся яго ўдзельнікам." + }, + { + "id": "api.command_mute.no_channel.error", + "translation": "Не ўдалося знайсці ўказаны канал. Калі ласка, выкарыстоўвайце [дэскрыптар канала](https://docs.mattermost.com/messaging/managing-channels.html#naming-a-channel) для ідэнтыфікацыі каналаў." + }, + { + "id": "api.command_mute.error", + "translation": "Не ўдалося знайсці канал {{.Channel}}. Для ідэнтыфікацыі каналаў выкарыстоўвайце [дэскрыптар канала](https://docs.mattermost.com/channels/channel-naming-conventions.html)." + }, + { + "id": "api.command_mute.desc", + "translation": "Адключыць апавяшчэння на працоўным стале, электроннай пошце і push-паведамлення для бягучага канала або названага [канала]." + }, + { + "id": "api.command_msg.permission.app_error", + "translation": "У вас няма правоў для адпраўкі асабістага паведамлення гэтаму карыстачу." + }, + { + "id": "api.command_msg.missing.app_error", + "translation": "Карыстальнік не знойдзены." + }, + { + "id": "api.command_msg.fail.app_error", + "translation": "Адбылася памылка пры перадачы паведамлення карыстачу." + }, + { + "id": "api.command_msg.dm_fail.app_error", + "translation": "Пры стварэнні канала асабістых паведамленняў здарылася памылка." + }, + { + "id": "api.command_msg.desc", + "translation": "Адаслаць асабістае паведамленне карыстальніку" + }, + { + "id": "api.command_leave.fail.app_error", + "translation": "Падчас пакідання канала адбылася памылка." + }, + { + "id": "api.command_leave.desc", + "translation": "Пакінуць бягучы канал" + }, + { + "id": "api.command_join.missing.app_error", + "translation": "Не ўдалося знайсці канал." + }, + { + "id": "api.command_join.list.app_error", + "translation": "Пры пералічэнні каналаў адбылася памылка." + }, + { + "id": "api.license.request_trial_license.embargoed", + "translation": "Мы не змаглі апрацаваць запыт з-за абмежаванняў для краін, на якія распаўсюджваецца эмбарга. [Даведацца больш у нашай дакументацыі](https://mattermost.com/pl/limitations-for-embargoed-countries) або звяртайцеся па адрасе legal@mattermost.com па пытаннях адносна абмежаванняў на экспарт." + }, + { + "id": "api.getThreadsForUser.bad_only_params", + "translation": "Параметры OnlyThreads і OnlyTotals ўзаемавыключальныя з getThreadsForUser" + }, + { + "id": "api.file.test_connection_s3_settings_nil.app_error", + "translation": "Налады захоўвання файлаў маюць неўсталяваныя значэнні." + }, + { + "id": "api.file.test_connection_email_settings_nil.app_error", + "translation": "Налады электроннай пошты маюць неўсталяваныя значэнні." + }, + { + "id": "model.channel.is_valid.1_or_more.app_error", + "translation": "Імя павінна складацца з 1 або больш малога літару і лічбаў." + }, + { + "id": "model.member.is_valid.emails.app_error", + "translation": "Спіс электроннай пошты пусты" + }, + { + "id": "model.member.is_valid.channel.app_error", + "translation": "Назва канала несапраўдная" + }, + { + "id": "api.templates.invite_team_and_channels_subject", + "translation": "[{{ .SiteName }}] {{ .SenderName }} запрашае вас далучыцца да каналаў {{ .ChannelsLen }} у камандзе {{ .TeamDisplayName }}" + }, + { + "id": "api.templates.invite_team_and_channels_body.title", + "translation": "{{ .SenderName }} запрасіў вас далучыцца да каналаў {{ .ChannelsLen }} у камандзе {{ .TeamDisplayName }}" + }, + { + "id": "api.templates.invite_team_and_channel_subject", + "translation": "[{{ .SiteName }}] {{ .SenderName }} запрашае вас далучыцца да {{ .ChannelName }} у камандзе {{ .TeamDisplayName }}" + }, + { + "id": "api.templates.invite_team_and_channel_body.title", + "translation": "{{ .SenderName }} запрасіў вас далучыцца да {{ .ChannelName }} у камандзе {{ .TeamDisplayName }}" + }, + { + "id": "api.team.invite_members_to_team_and_channels.invalid_body_parsing.app_error", + "translation": "Памылка пры аналізе даных цела." + }, + { + "id": "api.team.invite_members_to_team_and_channels.invalid_body.app_error", + "translation": "Несапраўднае цела запыту." + }, + { + "id": "api.templates.server_inactivity_footer_disclaimer", + "translation": "Вы атрымалі гэты аднаразовы ліст, таму што ваш сервер Mattermost быў неактыўны больш за {{.Hours}} гадзін. Гэты электронны ліст быў аўтаматычна створаны вашым серверам Mattermost." + }, + { + "id": "api.elasticsearch.test_elasticsearch_settings_nil.app_error", + "translation": "Налады Elasticsearch маюць неўсталяваныя значэнні." + }, + { + "id": "api.templates.cloud_upgrade_confirmation.title", + "translation": "Цяпер Ваш сервер абноўлены!" + }, + { + "id": "api.templates.cloud_upgrade_confirmation.subject", + "translation": "Пацвярджэнне абнаўлення Mattermost" + }, + { + "id": "app.user.update_thread_read_for_user_by_post.app_error", + "translation": "Недапушчальны post_id" + } +] diff --git a/server/i18n/bg.json b/server/i18n/bg.json index 3ed19971aaa..fcd20949389 100644 --- a/server/i18n/bg.json +++ b/server/i18n/bg.json @@ -535,10 +535,6 @@ "id": "model.file_info.is_valid.create_at.app_error", "translation": "Невалидна стойност за create_at." }, - { - "id": "model.file_info.get.gif.app_error", - "translation": "Gif-а не може да се декодира." - }, { "id": "model.emoji.user_id.app_error", "translation": "Невалиден id на създател." diff --git a/server/i18n/bn.json b/server/i18n/bn.json new file mode 100644 index 00000000000..0967ef424bc --- /dev/null +++ b/server/i18n/bn.json @@ -0,0 +1 @@ +{} diff --git a/server/i18n/br.json b/server/i18n/br.json new file mode 100644 index 00000000000..b74456cbdf0 --- /dev/null +++ b/server/i18n/br.json @@ -0,0 +1,126 @@ +[ + { + "id": "api.admin.test_email.missing_server", + "translation": "Ezhomm 'zeus eus ur servijer SMTP" + }, + { + "id": "api.admin.test_email.body", + "translation": "Evit doare eo staliet mat ho postel Mattermost !" + }, + { + "id": "api.admin.saml.set_certificate_from_metadata.missing_content_type.app_error", + "translation": "Ar seurt endalc'had a vank." + }, + { + "id": "api.admin.saml.set_certificate_from_metadata.invalid_content_type.app_error", + "translation": "N'eo ket reizh ar seurt endalc'had." + }, + { + "id": "api.admin.saml.set_certificate_from_metadata.invalid_body.app_error", + "translation": "Nammet eo testenn an testeni." + }, + { + "id": "api.admin.saml.not_available.app_error", + "translation": "SAML 2.0 n'eo na kefluniet na gouzañvet war ar servijer-mañ." + }, + { + "id": "api.admin.saml.failure_save_idp_certificate_file.app_error", + "translation": "N'eus ket bet tu mirout ar restr testeni." + }, + { + "id": "api.admin.remove_certificate.delete.app_error", + "translation": "Fazi en un distrujañ an testeni." + }, + { + "id": "api.admin.ldap.not_available.app_error", + "translation": "LDAP dihegerz." + }, + { + "id": "api.admin.get_brand_image.storage.app_error", + "translation": "Enrollañ skeudennoù n'eo ket kefluniet." + }, + { + "id": "api.admin.file_read_error", + "translation": "Fazi en ur lenn renabl ar fazioù." + }, + { + "id": "api.admin.delete_brand_image.storage.not_found", + "translation": "N'eus ket bet tu distrujañ ar skeudenn-merk, n'eo ket bet kavet." + }, + { + "id": "api.admin.add_certificate.saving.app_error", + "translation": "N'eus ket bet tu mirout ar restr testeni." + }, + { + "id": "api.admin.add_certificate.parseform.app_error", + "translation": "fazi o tielfennañ ur reked liesstumm" + }, + { + "id": "api.admin.add_certificate.open.app_error", + "translation": "N'eus ket bet tu digeriñ ar restr testeni." + }, + { + "id": "api.admin.add_certificate.no_file.app_error", + "translation": "Restr ebet dindan 'testeni' er reked." + }, + { + "id": "api.admin.add_certificate.array.app_error", + "translation": "Restr ebet dindan 'testeni' er reked." + }, + { + "id": "September", + "translation": "Miz Gwengolo" + }, + { + "id": "October", + "translation": "Miz Here" + }, + { + "id": "November", + "translation": "Miz Du" + }, + { + "id": "May", + "translation": "Miz Mae" + }, + { + "id": "March", + "translation": "Miz Meurzh" + }, + { + "id": "June", + "translation": "Miz Mezheven" + }, + { + "id": "July", + "translation": "Miz Gouere" + }, + { + "id": "January", + "translation": "Miz Genver" + }, + { + "id": "February", + "translation": "Miz C'hwevrer" + }, + { + "id": "December", + "translation": "Miz Kerzu" + }, + { + "id": "Channels", + "translation": "Chadennoù" + }, + { + "id": "Boards", + "translation": "Burevioù" + }, + { + "id": "August", + "translation": "Miz Eost" + }, + { + "id": "April", + "translation": "Miz Ebrel" + } +] diff --git a/server/i18n/ca.json b/server/i18n/ca.json new file mode 100644 index 00000000000..41a280660b3 --- /dev/null +++ b/server/i18n/ca.json @@ -0,0 +1,4762 @@ +[ + { + "id": "web.incoming_webhook.user.app_error", + "translation": "No s'ha pogut trobar l'usuari" + }, + { + "id": "web.incoming_webhook.invalid.app_error", + "translation": "Mètode no vàlid" + }, + { + "id": "web.incoming_webhook.disabled.app_error", + "translation": "L'administrador del sistema ha desactivat els «webhooks» entrants." + }, + { + "id": "web.incoming_webhook.channel.app_error", + "translation": "No s'ha pogut trobar el canal" + }, + { + "id": "web.command_webhook.command.app_error", + "translation": "No s'ha pogut trobar l'ordre" + }, + { + "id": "app.reaction.save.save.app_error", + "translation": "No es pot desar la reacció" + }, + { + "id": "app.emoji.delete.app_error", + "translation": "No es pot eliminar l'emoji" + }, + { + "id": "store.sql_command.save.get.app_error", + "translation": "No es pot obtenir l'ordre" + }, + { + "id": "app.channel.update.bad_id", + "translation": "No es pot actualitzar el canal" + }, + { + "id": "store.sql_channel.save_channel.limit.app_error", + "translation": "Heu assolit el límit del nombre de canals permesos." + }, + { + "id": "store.sql_channel.save.archived_channel.app_error", + "translation": "No pots modificar un canal arxivat" + }, + { + "id": "app.channel.get_deleted.existing.app_error", + "translation": "No es pot trobar el canal eliminat existent" + }, + { + "id": "store.sql_channel.get.existing.app_error", + "translation": "No es pot trobar el canal existent" + }, + { + "id": "app.audit.save.saving.app_error", + "translation": "Hem trobat un error en desar les auditories" + }, + { + "id": "app.audit.permanent_delete_by_user.app_error", + "translation": "Hem trobat un error en eliminar les auditories" + }, + { + "id": "app.audit.get.finding.app_error", + "translation": "Hem trobat un error en cercar les auditories" + }, + { + "id": "plugin_api.get_file_link.no_post.app_error", + "translation": "No es pot obtenir un enllaç públic per al fitxer. El fitxer s'ha d'adjuntar a una publicació que es pugui llegir." + }, + { + "id": "plugin_api.get_file_link.disabled.app_error", + "translation": "S'han desactivat els enllaços públics" + }, + { + "id": "model.user_access_token.is_valid.user_id.app_error", + "translation": "ID d'usuari no vàlid" + }, + { + "id": "model.user_access_token.is_valid.token.app_error", + "translation": "Testimoni d'accés no vàlid" + }, + { + "id": "model.user_access_token.is_valid.id.app_error", + "translation": "Valor no vàlid per a l'id." + }, + { + "id": "model.user.is_valid.update_at.app_error", + "translation": "«Update at» ha de ser un temps vàlid" + }, + { + "id": "model.user.is_valid.nickname.app_error", + "translation": "Àlies no vàlid" + }, + { + "id": "model.user.is_valid.last_name.app_error", + "translation": "Cognom no vàlid" + }, + { + "id": "model.user.is_valid.id.app_error", + "translation": "ID d'usuari no vàlid" + }, + { + "id": "model.user.is_valid.first_name.app_error", + "translation": "Nom no vàlid" + }, + { + "id": "model.user.is_valid.create_at.app_error", + "translation": "«Create at» ha de ser un temps vàlid" + }, + { + "id": "model.token.is_valid.size", + "translation": "Testimoni no vàlid." + }, + { + "id": "model.team_member.is_valid.user_id.app_error", + "translation": "ID d'usuari no vàlid" + }, + { + "id": "model.team_member.is_valid.team_id.app_error", + "translation": "ID d'equip no vàlid" + }, + { + "id": "model.team.is_valid.update_at.app_error", + "translation": "«Update at» ha de ser un temps vàlid" + }, + { + "id": "model.team.is_valid.type.app_error", + "translation": "Tipus no vàlid" + }, + { + "id": "model.team.is_valid.name.app_error", + "translation": "Nom no vàlid" + }, + { + "id": "model.team.is_valid.id.app_error", + "translation": "ID no vàlid" + }, + { + "id": "model.team.is_valid.description.app_error", + "translation": "Descripció no vàlida" + }, + { + "id": "model.team.is_valid.create_at.app_error", + "translation": "«Create at» ha de ser un temps vàlid" + }, + { + "id": "model.team.is_valid.company.app_error", + "translation": "Nom de l'empresa no vàlid" + }, + { + "id": "model.team.is_valid.characters.app_error", + "translation": "El nom ha de tenir 2 o més caràcters alfanumèrics en minúscula" + }, + { + "id": "model.reaction.is_valid.user_id.app_error", + "translation": "ID d'usuari no vàlid" + }, + { + "id": "model.reaction.is_valid.post_id.app_error", + "translation": "ID de publicació no vàlid" + }, + { + "id": "model.reaction.is_valid.emoji_name.app_error", + "translation": "Nom de l'emoji no vàlid" + }, + { + "id": "model.reaction.is_valid.create_at.app_error", + "translation": "«Create at» ha de ser un temps vàlid" + }, + { + "id": "model.preference.is_valid.theme.app_error", + "translation": "Tema no vàlid" + }, + { + "id": "model.preference.is_valid.name.app_error", + "translation": "Nom no vàlid" + }, + { + "id": "model.preference.is_valid.id.app_error", + "translation": "ID d'usuari no vàlid" + }, + { + "id": "model.post.is_valid.user_id.app_error", + "translation": "ID d'usuari no vàlid" + }, + { + "id": "model.post.is_valid.update_at.app_error", + "translation": "«Update at» ha de ser un temps vàlid" + }, + { + "id": "model.post.is_valid.type.app_error", + "translation": "Tipus no vàlid" + }, + { + "id": "model.post.is_valid.root_id.app_error", + "translation": "ID d'arrel no vàlid" + }, + { + "id": "model.post.is_valid.props.app_error", + "translation": "Props no vàlid" + }, + { + "id": "model.post.is_valid.msg.app_error", + "translation": "Estat no vàlid" + }, + { + "id": "model.post.is_valid.id.app_error", + "translation": "ID no vàlid" + }, + { + "id": "model.post.is_valid.filenames.app_error", + "translation": "Nom de fitxers no vàlids" + }, + { + "id": "model.post.is_valid.create_at.app_error", + "translation": "«Create at» ha de ser un temps vàlid" + }, + { + "id": "model.post.is_valid.channel_id.app_error", + "translation": "ID del canal no vàlid" + }, + { + "id": "model.outgoing_hook.username.app_error", + "translation": "Nom d'usuari no vàlid." + }, + { + "id": "model.outgoing_hook.is_valid.user_id.app_error", + "translation": "ID d'usuari no vàlid" + }, + { + "id": "model.outgoing_hook.is_valid.url.app_error", + "translation": "L'URL de resposta no és vàlid. Cadascun ha de ser un URL vàlid i començar per http:// o https://" + }, + { + "id": "model.outgoing_hook.is_valid.update_at.app_error", + "translation": "«Update at» ha de ser un temps vàlid" + }, + { + "id": "model.outgoing_hook.is_valid.token.app_error", + "translation": "Testimoni no vàlid." + }, + { + "id": "model.outgoing_hook.is_valid.team_id.app_error", + "translation": "ID d'equip no vàlid" + }, + { + "id": "model.outgoing_hook.is_valid.id.app_error", + "translation": "ID no vàlid" + }, + { + "id": "model.outgoing_hook.is_valid.display_name.app_error", + "translation": "Títol no vàlid" + }, + { + "id": "model.outgoing_hook.is_valid.description.app_error", + "translation": "Descripció no vàlida" + }, + { + "id": "model.outgoing_hook.is_valid.create_at.app_error", + "translation": "«Create at» ha de ser un temps vàlid" + }, + { + "id": "model.outgoing_hook.is_valid.channel_id.app_error", + "translation": "ID del canal no vàlid" + }, + { + "id": "model.outgoing_hook.icon_url.app_error", + "translation": "Icona no vàlida" + }, + { + "id": "model.oauth.is_valid.update_at.app_error", + "translation": "«Update at» ha de ser un temps vàlid" + }, + { + "id": "model.oauth.is_valid.name.app_error", + "translation": "Nom no vàlid" + }, + { + "id": "model.oauth.is_valid.icon_url.app_error", + "translation": "L'URL de la icona ha de ser un URL vàlid i començar per http:// o https://." + }, + { + "id": "model.oauth.is_valid.homepage.app_error", + "translation": "La pàgina d'inici ha de ser un URL vàlid i començar per http:// o https://." + }, + { + "id": "model.oauth.is_valid.description.app_error", + "translation": "Descripció no vàlida" + }, + { + "id": "model.oauth.is_valid.creator_id.app_error", + "translation": "ID del creador no vàlid" + }, + { + "id": "model.oauth.is_valid.create_at.app_error", + "translation": "«Create at» ha de ser un temps vàlid" + }, + { + "id": "model.oauth.is_valid.callback.app_error", + "translation": "L'URL de retorn no és vàlid. Ha de ser un URL vàlid i començar per http:// o https://." + }, + { + "id": "model.oauth.is_valid.app_id.app_error", + "translation": "ID d'usuari no vàlid" + }, + { + "id": "model.job.is_valid.type.app_error", + "translation": "Tipus de treball no vàlid" + }, + { + "id": "model.job.is_valid.id.app_error", + "translation": "ID de treball no vàlid" + }, + { + "id": "model.job.is_valid.create_at.app_error", + "translation": "«Create at» ha de ser un temps vàlid" + }, + { + "id": "model.incoming_hook.username.app_error", + "translation": "Nom d'usuari no vàlid." + }, + { + "id": "model.incoming_hook.user_id.app_error", + "translation": "ID d'usuari no vàlid" + }, + { + "id": "model.incoming_hook.update_at.app_error", + "translation": "«Update at» ha de ser un temps vàlid" + }, + { + "id": "model.incoming_hook.team_id.app_error", + "translation": "ID d'equip no vàlid" + }, + { + "id": "model.incoming_hook.id.app_error", + "translation": "ID no vàlid" + }, + { + "id": "model.incoming_hook.icon_url.app_error", + "translation": "Icona de publicació no vàlida" + }, + { + "id": "model.incoming_hook.display_name.app_error", + "translation": "Títol no vàlid" + }, + { + "id": "model.incoming_hook.description.app_error", + "translation": "Descripció no vàlida" + }, + { + "id": "model.incoming_hook.create_at.app_error", + "translation": "«Create at» ha de ser un temps vàlid" + }, + { + "id": "model.incoming_hook.channel_id.app_error", + "translation": "ID del canal no vàlid" + }, + { + "id": "model.guest.is_valid.channels.app_error", + "translation": "ID del canal no vàlid" + }, + { + "id": "model.guest.is_valid.channel.app_error", + "translation": "ID del canal no vàlid" + }, + { + "id": "model.file_info.is_valid.user_id.app_error", + "translation": "Valor no vàlid per a l'«user_id»." + }, + { + "id": "model.file_info.is_valid.update_at.app_error", + "translation": "Valor no vàlid per a «update_at»." + }, + { + "id": "model.file_info.is_valid.post_id.app_error", + "translation": "Valor no vàlid per al «post_id»." + }, + { + "id": "model.file_info.is_valid.path.app_error", + "translation": "Valor no vàlid per a la ruta." + }, + { + "id": "model.file_info.is_valid.id.app_error", + "translation": "Valor no vàlid per a l'id." + }, + { + "id": "model.file_info.is_valid.create_at.app_error", + "translation": "Valor no vàlid per a «create_at»." + }, + { + "id": "model.emoji.user_id.app_error", + "translation": "ID del creador no vàlid" + }, + { + "id": "model.emoji.update_at.app_error", + "translation": "«Update at» ha de ser un temps vàlid" + }, + { + "id": "model.emoji.name.app_error", + "translation": "El nom ha de tenir entre 1 i 64 caràcters alfanumèrics en minúscula" + }, + { + "id": "model.emoji.id.app_error", + "translation": "ID d'emoji no vàlid" + }, + { + "id": "model.emoji.create_at.app_error", + "translation": "«Create at» ha de ser un temps vàlid" + }, + { + "id": "model.config.is_valid.write_timeout.app_error", + "translation": "El valor per al temps d'espera d'escriptura no és vàlid." + }, + { + "id": "model.config.is_valid.websocket_url.app_error", + "translation": "L'URL del Websocket ha de ser un URL vàlid i començar amb ws:// o wss://" + }, + { + "id": "model.config.is_valid.sql_query_timeout.app_error", + "translation": "Temps d'espera de consulta no vàlid per a la configuració SQL. Ha de ser un número positiu." + }, + { + "id": "model.config.is_valid.sql_max_conn.app_error", + "translation": "Connexió oberta màxima no vàlida per a la configuració SQL. Ha de ser un número positiu." + }, + { + "id": "model.config.is_valid.sql_idle.app_error", + "translation": "Connexió inactiva màxima no vàlida per a la configuració SQL. Ha de ser un número positiu." + }, + { + "id": "model.config.is_valid.sql_driver.app_error", + "translation": "El nom del controlador per a la configuració SQL no és vàlid. Ha de ser «mysql» o «postgres»" + }, + { + "id": "model.config.is_valid.site_url.app_error", + "translation": "L'URL del lloc ha de ser un URL vàlid i començar per http:// o https://" + }, + { + "id": "model.config.is_valid.saml_idp_url.app_error", + "translation": "L'URL de SAML SSO ha de ser un URL vàlid i començar per http:// o https://." + }, + { + "id": "model.config.is_valid.saml_idp_descriptor_url.app_error", + "translation": "L'URL de l'emissor del proveïdor d'identitat ha de ser un URL vàlid i començar per http:// o https://." + }, + { + "id": "model.config.is_valid.saml_assertion_consumer_service_url.app_error", + "translation": "L'URL d'inici de sessió del proveïdor de serveis ha de ser un URL vàlid i començar per http:// o https://." + }, + { + "id": "model.config.is_valid.restrict_direct_message.app_error", + "translation": "La restricció del missatge directe no és vàlida. Ha de ser 'any' o 'team'" + }, + { + "id": "model.config.is_valid.read_timeout.app_error", + "translation": "El valor per al temps d'espera de lectura no és vàlid." + }, + { + "id": "model.config.is_valid.password_length.app_error", + "translation": "La longitud mínima de la contrasenya ha de ser un nombre sencer superior o igual a {{.MinLength}} i menor o igual a {{.MaxLength}}." + }, + { + "id": "model.config.is_valid.max_users.app_error", + "translation": "Els usuaris màxims no són vàlids per equip per a la configuració de l'equip. Ha de ser un nombre positiu." + }, + { + "id": "model.config.is_valid.max_notify_per_channel.app_error", + "translation": "Les notificacions màximes per canal no són vàlides per a la configuració de l'equip. Ha de ser un nombre positiu." + }, + { + "id": "model.config.is_valid.max_file_size.app_error", + "translation": "Mida màxima del fitxer no vàlida per a la configuració del fitxer. Ha de ser un número sencer superior a zero." + }, + { + "id": "model.config.is_valid.max_channels.app_error", + "translation": "Els canals màxims per equip no són vàlids per a la configuració de l'equip. Ha de ser un número positiu." + }, + { + "id": "model.config.is_valid.max_burst.app_error", + "translation": "La mida màxima de la ràfega ha de ser superior a zero." + }, + { + "id": "model.config.is_valid.login_attempts.app_error", + "translation": "Els intents d'inici de sessió màxim no són vàlids per a la configuració del servei. Ha de ser un número positiu." + }, + { + "id": "model.config.is_valid.localization.available_locales.app_error", + "translation": "Els idiomes disponibles han de contenir l'idioma per defecte del client" + }, + { + "id": "model.config.is_valid.listen_address.app_error", + "translation": "Adreça d’escolta no vàlida per a la configuració del servei. S'ha d'establir." + }, + { + "id": "model.config.is_valid.ldap_username", + "translation": "El camp AD/LDAP «Username Attribute» és obligatori." + }, + { + "id": "model.config.is_valid.ldap_sync_interval.app_error", + "translation": "Temps d’interval de sincronització no vàlid. Cal que sigui almenys un minut." + }, + { + "id": "model.config.is_valid.ldap_server", + "translation": "El camp AD/LDAP «AD/LDAP Server» és obligatori." + }, + { + "id": "model.config.is_valid.ldap_security.app_error", + "translation": "La seguretat de la connexió no és vàlida per a la configuració AD/LDAP. Ha de ser '', 'TLS' o 'STARTTLS'" + }, + { + "id": "model.config.is_valid.ldap_max_page_size.app_error", + "translation": "El valor de la mida de la pàgina no és vàlid." + }, + { + "id": "model.config.is_valid.ldap_login_id", + "translation": "El camp AD/LDAP «Login ID Attribute» és obligatori." + }, + { + "id": "model.config.is_valid.ldap_id", + "translation": "El campo AD/LDAP «ID Attribute» és obligatori." + }, + { + "id": "model.config.is_valid.ldap_email", + "translation": "El camp AD/LDAP «Email Attribute» és obligatori." + }, + { + "id": "model.config.is_valid.ldap_basedn", + "translation": "El camp AD/LDAP «BaseDN» és obligatori." + }, + { + "id": "ent.elasticsearch.search_users.search_failed", + "translation": "No s'ha pogut completar la cerca" + }, + { + "id": "ent.elasticsearch.search_posts.unmarshall_post_failed", + "translation": "No s'ha pogut descodificar els resultats de la cerca" + }, + { + "id": "ent.elasticsearch.search_posts.search_failed", + "translation": "No s'ha pogut completar la cerca" + }, + { + "id": "ent.elasticsearch.search_posts.parse_matches_failed", + "translation": "No s'ha pogut analitzar les coincidències del resultat de la cerca" + }, + { + "id": "ent.elasticsearch.search_posts.disabled", + "translation": "La cerca d'Elasticsearch està desactivada en aquest servidor" + }, + { + "id": "ent.elasticsearch.search_channels.unmarshall_channel_failed", + "translation": "No s'ha pogut descodificar els resultats de la cerca" + }, + { + "id": "ent.elasticsearch.search_channels.search_failed", + "translation": "No s'ha pogut completar la cerca" + }, + { + "id": "ent.elasticsearch.search_channels.disabled", + "translation": "La cerca d'Elasticsearch està desactivada en aquest servidor" + }, + { + "id": "ent.elasticsearch.refresh_indexes.refresh_failed", + "translation": "No s'han pogut obtenir els índexs d'Elasticsearch" + }, + { + "id": "ent.elasticsearch.purge_indexes.delete_failed", + "translation": "No s'ha pogut suprimir l'índex d'Elasticsearch" + }, + { + "id": "ent.elasticsearch.not_started.error", + "translation": "L'Elasticsearch no s'ha iniciat" + }, + { + "id": "ent.elasticsearch.indexer.index_batch.nothing_left_to_index.error", + "translation": "S'està intentant indexar un nou lot quan es completin totes les entitats." + }, + { + "id": "ent.elasticsearch.indexer.do_job.parse_start_time.error", + "translation": "El treball d'indexació d'Elasticsearch no va poder analitzar la hora d'inici" + }, + { + "id": "ent.elasticsearch.indexer.do_job.parse_end_time.error", + "translation": "El treball d'indexació d'Elasticsearch no ha pogut analitzar l'hora de finalització" + }, + { + "id": "ent.elasticsearch.index_user.error", + "translation": "No s'ha pogut indexar l'usuari" + }, + { + "id": "ent.elasticsearch.index_post.error", + "translation": "No s'ha pogut indexar la publicació" + }, + { + "id": "ent.elasticsearch.index_channel.error", + "translation": "No s'ha pogut indexar el canal" + }, + { + "id": "ent.elasticsearch.generic.disabled", + "translation": "La cerca d'Elasticsearch no està activada en aquest servidor" + }, + { + "id": "ent.elasticsearch.delete_user.error", + "translation": "No s'ha pogut eliminar l'usuari" + }, + { + "id": "ent.elasticsearch.delete_post.error", + "translation": "No s'ha pogut eliminar la publicació" + }, + { + "id": "ent.elasticsearch.delete_channel.error", + "translation": "No s'ha pogut suprimir el canal" + }, + { + "id": "ent.elasticsearch.data_retention_delete_indexes.get_indexes.error", + "translation": "No s'han pogut obtenir els índexs d'Elasticsearch" + }, + { + "id": "ent.elasticsearch.data_retention_delete_indexes.delete_index.error", + "translation": "No s'ha pogut suprimir l'índex d'Elasticsearch" + }, + { + "id": "ent.elasticsearch.create_client.connect_failed", + "translation": "S'ha produït un error en configurar el client Elasticsearch" + }, + { + "id": "ent.elasticsearch.aggregator_worker.index_job_failed.error", + "translation": "L'agregador de treballs Elasticsearch va fallar a causa de que hi ha un treball d'indexació fallant" + }, + { + "id": "ent.elasticsearch.aggregator_worker.get_indexes.error", + "translation": "L'agregador de treballs Elasticsearch no va poder obtenir els índexs" + }, + { + "id": "ent.elasticsearch.aggregator_worker.delete_indexes.error", + "translation": "L'agregador de treballs Elasticsearch no va poder eliminar els índexs" + }, + { + "id": "ent.elasticsearch.aggregator_worker.create_index_job.error", + "translation": "L'agregador de treballs Elasticsearch no va poder crear el treball d'indexació" + }, + { + "id": "ent.data_retention.generic.license.error", + "translation": "La teva llicència no és compatible amb la retenció de dades." + }, + { + "id": "ent.compliance.run_failed.error", + "translation": "El treball '{{.JobName}}' ha fallat en exportar la conformitat a '{{.FilePath}}'" + }, + { + "id": "ent.compliance.run_export.template_watcher.appError", + "translation": "No es poden carregar les plantilles d'exportació. Torna-ho a provar." + }, + { + "id": "ent.compliance.licence_disable.app_error", + "translation": "La funció de compliment està desactivada per la llicència actual. Contacta amb l'administrador del sistema per actualitzar la teva llicència empresarial." + }, + { + "id": "ent.compliance.global_relay.rewind_temporary_file.appError", + "translation": "No es pot tornar a llegir el fitxer d'exportació temporal Global Relay." + }, + { + "id": "ent.compliance.global_relay.open_temporary_file.appError", + "translation": "No es pot obrir el fitxer temporal d'exportació." + }, + { + "id": "ent.compliance.global_relay.attachments_removed.appError", + "translation": "El fitxer pujat s'ha eliminat de l'exportació Global Relay perquè era massa gran per enviar-lo." + }, + { + "id": "ent.compliance.csv.zip.creation.appError", + "translation": "No es pot crear el fitxer d'exportació zip." + }, + { + "id": "ent.compliance.csv.warning.appError", + "translation": "No es pot crear el fitxer eml." + }, + { + "id": "ent.compliance.csv.post.export.appError", + "translation": "No es pot exportar una publicació." + }, + { + "id": "ent.compliance.csv.metadata.json.zipfile.appError", + "translation": "No es pot crear el fitxer eml." + }, + { + "id": "ent.compliance.csv.metadata.json.marshalling.appError", + "translation": "No es poden convertir les metadades a json." + }, + { + "id": "ent.compliance.csv.metadata.export.appError", + "translation": "No es pot afegir el fitxer de metadades al fitxer zip." + }, + { + "id": "ent.compliance.csv.header.export.appError", + "translation": "No es pot afegir la capçalera a l'exportació CSV." + }, + { + "id": "ent.compliance.csv.file.creation.appError", + "translation": "No es pot crear el fitxer temporal d'exportació CSV." + }, + { + "id": "ent.compliance.csv.attachment.export.appError", + "translation": "No es pot afegir un fitxer adjunt a l'exportació CSV." + }, + { + "id": "ent.compliance.csv.attachment.copy.appError", + "translation": "No es pot copiar el fitxer adjunt al fitxer zip." + }, + { + "id": "ent.compliance.bad_export_type.appError", + "translation": "Format de sortida desconegut {{.ExportType}}" + }, + { + "id": "ent.cluster.save_config.error", + "translation": "La consola del sistema s'estableix a només lectura quan s'activa l'alta disponibilitat a menys que «ReadOnlyConfig» estigui desactivat al fitxer de configuració." + }, + { + "id": "ent.cluster.config_changed.info", + "translation": "La configuració del clúster ha canviat per a l'id={{ .id}}. El clúster pot esdevenir inestable i caldrà reiniciar-lo. Per assegurar-te que el clúster estigui configurat correctament, hauràs de fer un reinici continu immediatament." + }, + { + "id": "ent.account_migration.get_saml_users_failed", + "translation": "No es pot obtenir els usuaris SAML." + }, + { + "id": "ent.account_migration.get_all_failed", + "translation": "No es pot obtenir els usuaris." + }, + { + "id": "brand.save_brand_image.save_image.app_error", + "translation": "No es pot escriure el fitxer d'imatge al vostre emmagatzematge de fitxers. Comprova la teva connexió i torna-ho a provar." + }, + { + "id": "brand.save_brand_image.open.app_error", + "translation": "No es pot pujar la imatge de marca personalitzada. Assegura't que la mida de la imatge sigui inferior a 2 MB i torna-ho a provar." + }, + { + "id": "brand.save_brand_image.encode.app_error", + "translation": "No es pot convertir les dades de la imatge en format PNG. Torna-ho a provar." + }, + { + "id": "brand.save_brand_image.decode.app_error", + "translation": "No es pot descodificar les dades de la imatge." + }, + { + "id": "bleveengine.search_users_in_team.error", + "translation": "No s'ha pogut completar la cerca" + }, + { + "id": "bleveengine.search_users_in_channel.uchan.error", + "translation": "No s'ha pogut completar la cerca" + }, + { + "id": "bleveengine.search_users_in_channel.nuchan.error", + "translation": "No s'ha pogut completar la cerca" + }, + { + "id": "bleveengine.search_posts.error", + "translation": "No s'ha pogut completar la cerca" + }, + { + "id": "bleveengine.search_channels.error", + "translation": "No s'ha pogut completar la cerca" + }, + { + "id": "bleveengine.indexer.index_batch.nothing_left_to_index.error", + "translation": "S'està intentant indexar un nou lot quan es completin totes les entitats." + }, + { + "id": "bleveengine.indexer.do_job.parse_start_time.error", + "translation": "El treball d'indexació d'Elasticsearch no va poder analitzar la hora d'inici" + }, + { + "id": "bleveengine.indexer.do_job.parse_end_time.error", + "translation": "El treball d'indexació d'Elasticsearch no ha pogut analitzar l'hora de finalització" + }, + { + "id": "bleveengine.index_user.error", + "translation": "No s'ha pogut indexar l'usuari" + }, + { + "id": "bleveengine.index_post.error", + "translation": "No s'ha pogut indexar la publicació" + }, + { + "id": "bleveengine.index_channel.error", + "translation": "No s'ha pogut indexar el canal" + }, + { + "id": "bleveengine.delete_user.error", + "translation": "No s'ha pogut eliminar l'usuari" + }, + { + "id": "bleveengine.delete_post.error", + "translation": "No s'ha pogut eliminar la publicació" + }, + { + "id": "bleveengine.delete_channel.error", + "translation": "No s'ha pogut suprimir el canal" + }, + { + "id": "app.user_access_token.invalid_or_missing", + "translation": "Testimoni no vàlid o absent" + }, + { + "id": "app.user_access_token.disabled", + "translation": "S'han desactivat els testimonis d'accés personal en aquest servidor. Contacta amb l'administrador del sistema per obtenir-ne més detalls." + }, + { + "id": "app.team.join_user_to_team.max_accounts.app_error", + "translation": "Aquest equip ha arribat al número màxim de comptes permesos. Contacta amb l'administrador del sistema per establir un límit superior." + }, + { + "id": "app.system_install_date.parse_int.app_error", + "translation": "No s'ha pogut analitzar la data d'instal·lació" + }, + { + "id": "app.submit_interactive_dialog.json_error", + "translation": "S'ha trobat un error de codificació JSON per al diàleg interactiu." + }, + { + "id": "app.schemes.is_phase_2_migration_completed.not_completed.app_error", + "translation": "Aquest punt final de l'API no és accessible perquè les migracions necessàries encara no s'han completat." + }, + { + "id": "app.save_config.app_error", + "translation": "S'ha produït un error en desar la configuració" + }, + { + "id": "app.role.check_roles_exist.role_not_found", + "translation": "El rol proporcionat no existeix" + }, + { + "id": "app.plugin.write_file.saving.app_error", + "translation": "S'ha produït un error en deixar del canal." + }, + { + "id": "app.plugin.write_file.read.app_error", + "translation": "S'ha produït un error en deixar del canal." + }, + { + "id": "app.plugin.upload_disabled.app_error", + "translation": "Els connectors i/o les pujades dels connectors s'han desactivat." + }, + { + "id": "app.plugin.remove.app_error", + "translation": "No es pot eliminar el connector" + }, + { + "id": "app.plugin.not_installed.app_error", + "translation": "El connector no està instal·lat" + }, + { + "id": "app.plugin.mvdir.app_error", + "translation": "No es pot moure el connector des del directori temporal fins a la destinació final. Un altre complement pot estar utilitzant el mateix nom de directori." + }, + { + "id": "app.plugin.marketplace_disabled.app_error", + "translation": "Els connectors s'han desactivat. Verifiqueu els teus registres per obtenir-ne més detalls." + }, + { + "id": "app.plugin.manifest.app_error", + "translation": "No es pot trobar el manifest per al connector extret" + }, + { + "id": "app.plugin.invalid_id.app_error", + "translation": "L'ID del connector ha de tenir un mínim de {{.Min}} caràcters i un màxim de {{.Max}} caràcters i coincidir amb {{.Regex}}." + }, + { + "id": "app.plugin.install_id_failed_remove.app_error", + "translation": "No es pot instal·lar el connector. Ja s'ha instal·lat un connector amb el mateix ID i no s'ha pogut eliminar." + }, + { + "id": "app.plugin.install_id.app_error", + "translation": "No es pot instal·lar el connector. Ja s'ha instal·lat un connector amb el mateix ID." + }, + { + "id": "app.plugin.install.app_error", + "translation": "No es pot instal·lar el connector." + }, + { + "id": "app.plugin.get_statuses.app_error", + "translation": "No es poden obtenir els estats dels connectors" + }, + { + "id": "app.plugin.get_plugins.app_error", + "translation": "No es pot obtenir els connectors actius" + }, + { + "id": "app.plugin.get_cluster_plugin_statuses.app_error", + "translation": "No es poden obtenir els estats dels connectors del clúster." + }, + { + "id": "app.plugin.filesystem.app_error", + "translation": "S'ha detectat un error del sistema de fitxers" + }, + { + "id": "app.plugin.disabled.app_error", + "translation": "Els connectors s'han desactivat. Verifiqueu els teus registres per obtenir-ne més detalls." + }, + { + "id": "app.plugin.delete_public_key.delete.app_error", + "translation": "S'ha produït un error en suprimir el certificat." + }, + { + "id": "app.plugin.deactivate.app_error", + "translation": "No es pot desactivar el connector" + }, + { + "id": "app.plugin.config.app_error", + "translation": "S'ha produït un error en desar l'estat del connector a la configuració" + }, + { + "id": "app.plugin.cluster.save_config.app_error", + "translation": "La configuració del connector al teu fitxer config.json s'ha d'actualitzar manualment quan s'utilitza «ReadOnlyConfig» amb el clúster activat." + }, + { + "id": "app.notification.subject.notification.full", + "translation": "[{{ .SiteName }}] Notificació a {{ .TeamName}} el {{.Day}} {{.Month}}, {{.Year}}" + }, + { + "id": "app.notification.subject.group_message.generic", + "translation": "[{{ .SiteName }}] Nou missatge de grup el {{.Day}} {{.Month}}, {{.Year}}" + }, + { + "id": "app.notification.subject.group_message.full", + "translation": "[{{ .SiteName }}] Nou missatge grupal a {{ .ChannelName}} el {{.Day}} {{.Month}}, {{.Year}}" + }, + { + "id": "app.notification.subject.direct.full", + "translation": "[{{.SiteName}}] Nou missatge directe de {{.SenderDisplayName}} el {{.Day}} {{.Month}}, {{.Year}}" + }, + { + "id": "app.import.validate_user_teams_import_data.team_name_missing.error", + "translation": "Falta el nom de l'equip a la subscripció de l'equip de l'usuari." + }, + { + "id": "app.import.validate_user_teams_import_data.invalid_roles.error", + "translation": "Rols no vàlids per a la subscripció de l'equip de l'usuari." + }, + { + "id": "app.import.validate_user_import_data.username_missing.error", + "translation": "Falta la propietat necessària de l'usuari: «username»." + }, + { + "id": "app.import.validate_user_import_data.username_invalid.error", + "translation": "El nom d'usuari no és vàlid." + }, + { + "id": "app.import.validate_user_import_data.roles_invalid.error", + "translation": "Els rols de l'usuari no són vàlids." + }, + { + "id": "app.import.validate_user_import_data.profile_image.error", + "translation": "Imatge de perfil no vàlida." + }, + { + "id": "app.import.validate_user_import_data.position_length.error", + "translation": "La posició de l'usuari és massa llarga." + }, + { + "id": "app.import.validate_user_import_data.password_length.error", + "translation": "La contrasenya de l'usuari té una longitud no vàlida." + }, + { + "id": "app.import.validate_user_import_data.notify_props_mobile_push_status_invalid.error", + "translation": "La propietat de notificació de l'estat de l'usuari per a rebre notificacions a dispositius mòbils no és vàlida." + }, + { + "id": "app.import.validate_user_import_data.notify_props_mobile_invalid.error", + "translation": "La propietat de notificació mòbil de l'usuari no és vàlida." + }, + { + "id": "app.import.validate_user_import_data.notify_props_email_invalid.error", + "translation": "La propietat de notificació de correu de l'usuari no és vàlida." + }, + { + "id": "app.import.validate_user_import_data.notify_props_desktop_sound_invalid.error", + "translation": "La propietat del so per a la notificació d'escriptori de l'usuari no és vàlida." + }, + { + "id": "app.import.validate_user_import_data.notify_props_desktop_invalid.error", + "translation": "La propietat de notificació d'escriptori de l'usuari no és vàlida." + }, + { + "id": "app.import.validate_user_import_data.notify_props_comments_trigger_invalid.error", + "translation": "La propietat «Comments» no és vàlida per a l'usuari." + }, + { + "id": "app.import.validate_user_import_data.notify_props_channel_trigger_invalid.error", + "translation": "La propietat de les paraules que activen les notificacions de l'usuari no és vàlida." + }, + { + "id": "app.import.validate_user_import_data.nickname_length.error", + "translation": "El sobrenom de l'usuari és massa llarg." + }, + { + "id": "app.import.validate_user_import_data.last_name_length.error", + "translation": "El cognom de l'usuari és massa llarg." + }, + { + "id": "app.import.validate_user_import_data.first_name_length.error", + "translation": "El nom de l'usuari és massa llarg." + }, + { + "id": "app.import.validate_user_import_data.email_missing.error", + "translation": "Falta la propietat necessària de l'usuari: «email»." + }, + { + "id": "app.import.validate_user_import_data.email_length.error", + "translation": "L'adreça de correu de l'usuari té una longitud no vàlida." + }, + { + "id": "app.import.validate_user_import_data.auth_data_length.error", + "translation": "La propietat «AuthData» de l'usuari és massa llarga." + }, + { + "id": "app.import.validate_user_import_data.auth_data_and_password.error", + "translation": "L'«AuthData» i la contrasenya de l'usuari són mútuament excloents." + }, + { + "id": "app.import.validate_user_import_data.advanced_props_email_interval.error", + "translation": "Configuració d'intervals de correu electrònic no vàlida per a l'Usuari" + }, + { + "id": "app.import.validate_user_channels_import_data.invalid_roles.error", + "translation": "Rols no vàlids per a la subscripció del canal de l'usuari." + }, + { + "id": "app.import.validate_user_channels_import_data.invalid_notify_props_mobile.error", + "translation": "Les propietats de notificació mòbils per a la subscripció del canal no són vàlides." + }, + { + "id": "app.import.validate_user_channels_import_data.invalid_notify_props_mark_unread.error", + "translation": "Les propietats de notificació no llegides per a la subscripció del canal no són vàlides." + }, + { + "id": "app.import.validate_user_channels_import_data.invalid_notify_props_desktop.error", + "translation": "Les propietats de notificació de l'escriptori per a la subscripció del canal no són vàlides." + }, + { + "id": "app.import.validate_user_channels_import_data.channel_name_missing.error", + "translation": "Falta el nom del canal a la subscripció del canal de l'usuari." + }, + { + "id": "app.import.validate_team_import_data.type_missing.error", + "translation": "Falta la propietat necessària de l'equip: «type»." + }, + { + "id": "app.import.validate_team_import_data.type_invalid.error", + "translation": "El tipus de l'equip no és vàlid." + }, + { + "id": "app.import.validate_team_import_data.scheme_invalid.error", + "translation": "El nom de l'esquema no és vàlid per a l'equip" + }, + { + "id": "app.import.validate_team_import_data.name_reserved.error", + "translation": "El nom de l'equip conté paraules reservades." + }, + { + "id": "app.import.validate_team_import_data.name_missing.error", + "translation": "Falta la propietat necessària de l'equip: «name»." + }, + { + "id": "app.import.validate_team_import_data.name_length.error", + "translation": "El nom de l'equip és massa llarg." + }, + { + "id": "app.import.validate_team_import_data.name_characters.error", + "translation": "El nom de de l'equip conté caràcters no vàlids." + }, + { + "id": "app.import.validate_team_import_data.display_name_missing.error", + "translation": "Falta la propietat necessària de l'equip: «display_name»" + }, + { + "id": "app.import.validate_team_import_data.display_name_length.error", + "translation": "El «display_name» de l'equpo no està dins de les restriccions de longitud permeses." + }, + { + "id": "app.import.validate_team_import_data.description_length.error", + "translation": "La descripció de l'equip és massa llarga." + }, + { + "id": "app.import.validate_scheme_import_data.wrong_roles_for_scope.error", + "translation": "Es van proporcionar els rols incorrectes per a un esquema amb aquest abast." + }, + { + "id": "app.import.validate_scheme_import_data.unknown_scheme.error", + "translation": "L'abast de l'esquema és desconegut." + }, + { + "id": "app.import.validate_scheme_import_data.null_scope.error", + "translation": "L'abast de l'esquema és obligatori." + }, + { + "id": "app.import.validate_scheme_import_data.name_invalid.error", + "translation": "Nom de l'esquema no vàlid." + }, + { + "id": "app.import.validate_scheme_import_data.display_name_invalid.error", + "translation": "Nom de visualització de l'esquema no vàlid." + }, + { + "id": "app.import.validate_scheme_import_data.description_invalid.error", + "translation": "Descripció de l'esquema no vàlid." + }, + { + "id": "app.scheme.save.app_error", + "translation": "No es pot crear l'esquema" + }, + { + "id": "app.scheme.get.app_error", + "translation": "No es pot obtenir l'esquema" + }, + { + "id": "app.import.validate_role_import_data.name_invalid.error", + "translation": "Nom del rol no vàlid." + }, + { + "id": "app.import.validate_role_import_data.invalid_permission.error", + "translation": "El permís no és vàlid per al rol." + }, + { + "id": "app.import.validate_role_import_data.display_name_invalid.error", + "translation": "Nom de visualització del rol no vàlid." + }, + { + "id": "app.import.validate_role_import_data.description_invalid.error", + "translation": "Descripció del rol no vàlida." + }, + { + "id": "app.import.validate_reply_import_data.user_missing.error", + "translation": "Falta la propietat necessària de la resposta: «User»." + }, + { + "id": "app.import.validate_reply_import_data.message_missing.error", + "translation": "Falta la propietat necessària de la resposta: «Message»." + }, + { + "id": "app.import.validate_reply_import_data.message_length.error", + "translation": "La propietat «Message» de la resposta és superior a la longitud màxima permesa." + }, + { + "id": "app.import.validate_reply_import_data.create_at_zero.error", + "translation": "La propietat «CreateAt» de la resposta no pot ser zero." + }, + { + "id": "app.import.validate_reply_import_data.create_at_missing.error", + "translation": "Falta la propietat necessària de la resposta: «create_at»." + }, + { + "id": "app.import.validate_reaction_import_data.user_missing.error", + "translation": "Falta la propietat necessària de la reacció: «User»." + }, + { + "id": "app.import.validate_reaction_import_data.emoji_name_missing.error", + "translation": "Falta la propietat necessària de la reacció: «EmojiName»." + }, + { + "id": "app.import.validate_reaction_import_data.emoji_name_length.error", + "translation": "La propietat «EmojiName» de la reacció supera la longitud màxima permesa." + }, + { + "id": "app.import.validate_reaction_import_data.create_at_zero.error", + "translation": "La propietat «CreateAt» de la reacció no pot ser zero." + }, + { + "id": "app.import.validate_reaction_import_data.create_at_missing.error", + "translation": "Falta la propietat necessària de la reacció: «create_at»." + }, + { + "id": "app.import.validate_reaction_import_data.create_at_before_parent.error", + "translation": "La propietat «CreateAt» de la reacció ha de ser major a la propietat «CreateAt» del missatge principal." + }, + { + "id": "app.import.validate_post_import_data.user_missing.error", + "translation": "Falta la propietat necessària del missatge: «User»." + }, + { + "id": "app.import.validate_post_import_data.team_missing.error", + "translation": "Falta la propietat necessària del missatge: «Team»." + }, + { + "id": "app.import.validate_post_import_data.message_missing.error", + "translation": "Falta la propietat necessària del missatge: «Message»." + }, + { + "id": "app.import.validate_post_import_data.message_length.error", + "translation": "La propietat «Message» de la publicació és superior a la longitud màxima permesa." + }, + { + "id": "app.import.validate_post_import_data.create_at_zero.error", + "translation": "La propietat «CreateAt» de la publicació no pot ser zero." + }, + { + "id": "app.import.validate_post_import_data.create_at_missing.error", + "translation": "Falta la propietat necessària del missatge: «create_at»." + }, + { + "id": "app.import.validate_post_import_data.channel_missing.error", + "translation": "Falta la propietat necessària del missatge: «channel»." + }, + { + "id": "app.import.validate_emoji_import_data.name_missing.error", + "translation": "Falta el camp del nom de l'emoji en la importació o està buit." + }, + { + "id": "app.import.validate_emoji_import_data.image_missing.error", + "translation": "Falta el camp d'imatge de l'emoji en la importació o està buit." + }, + { + "id": "app.import.validate_emoji_import_data.empty.error", + "translation": "Les dades d'importació de l'emoji estan buides." + }, + { + "id": "app.import.validate_direct_post_import_data.user_missing.error", + "translation": "Falta la propietat necessària del missatge: «user»" + }, + { + "id": "app.import.validate_direct_post_import_data.unknown_flagger.error", + "translation": "Els missatges dels canals directes només poden ser marcats pels seus membres. \"{{.Username}}\" no n'és un membre." + }, + { + "id": "app.import.validate_direct_post_import_data.message_missing.error", + "translation": "Falta la propietat necessària del missatge: «message»" + }, + { + "id": "app.import.validate_direct_post_import_data.message_length.error", + "translation": "El missatge és massa llarg" + }, + { + "id": "app.import.validate_direct_post_import_data.create_at_zero.error", + "translation": "«CreateAt» cal que sigui superior a 0" + }, + { + "id": "app.import.validate_direct_post_import_data.create_at_missing.error", + "translation": "Falta la propietat necessària del missatge: «create_at»" + }, + { + "id": "app.import.validate_direct_post_import_data.channel_members_too_many.error", + "translation": "La llista de membres del canal de publicació directa conté massa elements" + }, + { + "id": "app.import.validate_direct_post_import_data.channel_members_too_few.error", + "translation": "La llista de membres del canal de publicació directa conté molt pocs elements" + }, + { + "id": "app.import.validate_direct_post_import_data.channel_members_required.error", + "translation": "Falta la propietat necessària del canal directe: «channel_members»" + }, + { + "id": "app.import.validate_direct_channel_import_data.unknown_favoriter.error", + "translation": "Els canals directes només poden establir-se com a favorits pels seus membres. \"{{.Username}}\" no n'és un membre." + }, + { + "id": "app.import.validate_direct_channel_import_data.members_too_many.error", + "translation": "La llista de membres del canal directe conté massa ítems" + }, + { + "id": "app.import.validate_direct_channel_import_data.members_too_few.error", + "translation": "La llista de membres del canal directe conté molt pocs ítems" + }, + { + "id": "app.import.validate_direct_channel_import_data.members_required.error", + "translation": "Falta la propietat necessària del canal directe: «members»" + }, + { + "id": "app.import.validate_direct_channel_import_data.header_length.error", + "translation": "L'encapçalament del canal directe és massa llarg" + }, + { + "id": "app.import.validate_channel_import_data.type_missing.error", + "translation": "Falta la propietat necessària del canal: «type»" + }, + { + "id": "app.import.validate_channel_import_data.type_invalid.error", + "translation": "El tipus de canal no és vàlid." + }, + { + "id": "app.import.validate_channel_import_data.team_missing.error", + "translation": "Falta la propietat necessària del canal: «team»" + }, + { + "id": "app.import.validate_channel_import_data.scheme_invalid.error", + "translation": "El nom de l'esquema no és vàlid per al canal." + }, + { + "id": "app.import.validate_channel_import_data.purpose_length.error", + "translation": "El propòsit del canal és massa llarg." + }, + { + "id": "app.import.validate_channel_import_data.name_missing.error", + "translation": "Falta la propietat necessària del canal: «name»" + }, + { + "id": "app.import.validate_channel_import_data.name_length.error", + "translation": "El nom del canal és massa llarg." + }, + { + "id": "app.import.validate_channel_import_data.name_characters.error", + "translation": "El nom del canal conté caràcters no vàlids." + }, + { + "id": "app.import.validate_channel_import_data.header_length.error", + "translation": "L'encapçalament del canal és massa llarg." + }, + { + "id": "app.import.validate_channel_import_data.display_name_length.error", + "translation": "El «display_name» del canal no està dins de les restriccions de longitud permeses." + }, + { + "id": "api.user.send_verify_email_and_forget.failed.error", + "translation": "No s'ha pogut enviar el correu de verificació amb èxit" + }, + { + "id": "api.user.send_sign_in_change_email_and_forget.error", + "translation": "No s'ha pogut enviar el correu de contrasenya actualitzada amb èxit" + }, + { + "id": "api.user.send_password_reset.sso.app_error", + "translation": "No es pot restablir la contrasenya per als comptes SSO" + }, + { + "id": "api.user.send_password_reset.send.app_error", + "translation": "No s'ha pogut enviar el correu electrònic de restabliment de la contrasenya amb èxit" + }, + { + "id": "api.user.send_email_change_verify_email_and_forget.error", + "translation": "No s'ha pogut enviar el correu de verificació del canvi de correu amb èxit" + }, + { + "id": "api.user.send_deactivate_email_and_forget.failed.error", + "translation": "S'ha produït un error en enviar el correu de desactivació del compte" + }, + { + "id": "api.user.saml.not_available.app_error", + "translation": "SAML 2.0 no està configurat o no és compatible en aquest servidor." + }, + { + "id": "api.user.reset_password.token_parse.error", + "translation": "No s'ha pogut analitzar el testimoni de restabliment de contrasenya" + }, + { + "id": "api.user.reset_password.sso.app_error", + "translation": "No es pot restablir la contrasenya per als comptes SSO" + }, + { + "id": "api.user.reset_password.method", + "translation": "s'està utilitzant l'enllaç per restablir la contrasenya" + }, + { + "id": "api.user.reset_password.link_expired.app_error", + "translation": "L'enllaç de restabliment de la contrasenya ha caducat." + }, + { + "id": "api.user.reset_password.invalid_link.app_error", + "translation": "L'enllaç per restablir la contrasenya no sembla que sigui vàlid." + }, + { + "id": "api.user.reset_password.broken_token.app_error", + "translation": "El testimoni per restablir la contrasenya no sembla que sigui vàlid." + }, + { + "id": "api.user.oauth_to_email.not_available.app_error", + "translation": "La transferència d'autenticació no està configurada o disponible en aquest servidor." + }, + { + "id": "api.user.oauth_to_email.context.app_error", + "translation": "L'actualització de la contrasenya ha fallat perquè l'«user_id» del context no coincideix amb l'ID d'usuari proporcionat" + }, + { + "id": "api.user.login_ldap.not_available.app_error", + "translation": "L'AD/LDAP no està disponible en aquest servidor" + }, + { + "id": "api.user.login_by_oauth.parse.app_error", + "translation": "No s'ha pogut obtenir les dades d'autorització de l'objecte de {{.Service}}" + }, + { + "id": "api.user.login_by_oauth.not_available.app_error", + "translation": "{{.Service}} SSO a través d'OAuth 2.0 no està disponible en aquest servidor" + }, + { + "id": "api.user.login_by_oauth.bot_login_forbidden.app_error", + "translation": "L'inici de sessió com a bot està prohibit" + }, + { + "id": "api.user.login.use_auth_service.app_error", + "translation": "Inicia sessió utilitzant {{.AuthService}}" + }, + { + "id": "api.user.login.not_verified.app_error", + "translation": "Ha fallat l'inici de sessió perquè l'adreça de correu no s'ha verificat" + }, + { + "id": "api.user.login.invalid_credentials_username", + "translation": "Introdueix un usuari i/o contrasenya vàlids." + }, + { + "id": "api.user.login.invalid_credentials_sso", + "translation": "Introdueix un correu o usuari i/o contrasenya, o signa utilitzant algun altre mètode." + }, + { + "id": "api.user.login.invalid_credentials_email_username", + "translation": "Introdueix un correu o usuari i/o contrasenya vàlids." + }, + { + "id": "api.user.login.invalid_credentials_email", + "translation": "Introdueix un correu i/o contrasenya vàlids." + }, + { + "id": "api.user.login.inactive.app_error", + "translation": "Ha fallat l'inici de sessió perquè el teu compte s'ha desactivat. Contacta amb un administrador." + }, + { + "id": "api.user.login.guest_accounts.license.error", + "translation": "La teva llicència no suporta: Idap groups" + }, + { + "id": "api.user.login.guest_accounts.disabled.error", + "translation": "Els comptes de convidat estan desactivats" + }, + { + "id": "api.user.login.client_side_cert.license.app_error", + "translation": "S'ha intentat utilitzar la característica experimental ClientSideCert sense una llicència empresarial vàlida" + }, + { + "id": "api.user.login.client_side_cert.certificate.app_error", + "translation": "S'ha intentat iniciar sessió utilitzant la característica experimental ClientSideCert sense proveir un certificat vàlid" + }, + { + "id": "api.user.login.bot_login_forbidden.app_error", + "translation": "L'inici de sessió com a bot està prohibit" + }, + { + "id": "api.user.login.blank_pwd.app_error", + "translation": "El camp de la contrasenya no pot estar en blanc" + }, + { + "id": "api.user.ldap_to_email.not_ldap_account.app_error", + "translation": "Aquest compte d'usuari no utilitza AD/LDAP" + }, + { + "id": "api.user.ldap_to_email.not_available.app_error", + "translation": "L'AD/LDAP no està disponible en aquest servidor" + }, + { + "id": "api.user.get_user_by_email.permissions.app_error", + "translation": "No s'ha pogut obtenir l'usuari per correu." + }, + { + "id": "api.user.email_to_oauth.not_available.app_error", + "translation": "La transferència d'autenticació no està configurada o disponible en aquest servidor." + }, + { + "id": "api.user.email_to_ldap.not_available.app_error", + "translation": "L'AD/LDAP no està disponible en aquest servidor" + }, + { + "id": "api.user.create_user.signup_link_invalid.app_error", + "translation": "L'enllaç de registre no sembla que sigui vàlid" + }, + { + "id": "api.user.create_user.signup_link_expired.app_error", + "translation": "L'enllaç de registre ha caducat" + }, + { + "id": "api.user.create_user.signup_email_disabled.app_error", + "translation": "El registre d'usuari amb correu està desactivat." + }, + { + "id": "api.user.create_user.no_open_server", + "translation": "Aquest servidor no permet els registres oberts. Parla amb el teu administrador per rebre una invitació." + }, + { + "id": "api.user.create_user.guest_accounts.license.app_error", + "translation": "La teva llicència no suporta comptes de convidats" + }, + { + "id": "api.user.create_user.guest_accounts.disabled.app_error", + "translation": "Els comptes de convidats estan desactivats." + }, + { + "id": "api.user.create_user.disabled.app_error", + "translation": "La creació d'usuaris està desactivada." + }, + { + "id": "api.user.create_user.accepted_domain.app_error", + "translation": "El correu electrònic que has proporcionat no pertany a un domini permès. Contacta amb el teu administrador o registra't amb un correu diferent." + }, + { + "id": "api.user.create_profile_image.initial.app_error", + "translation": "No s'ha pogut assignar la inicial de l'usuari com a imatge del perfil per defecte" + }, + { + "id": "api.user.create_profile_image.encode.app_error", + "translation": "No s'ha pogut codificar la imatge del perfil per defecte" + }, + { + "id": "api.user.create_profile_image.default_font.app_error", + "translation": "No s'ha pogut crear la font per a la imatge del perfil per defecte" + }, + { + "id": "api.user.create_password_token.error", + "translation": "No es pot crear el testimoni de recuperació de contrasenyes." + }, + { + "id": "api.user.create_oauth_user.create.app_error", + "translation": "No s'ha pogut crear l'usuari basant-se en l'objecte de {{.Service}}" + }, + { + "id": "api.user.create_oauth_user.already_attached.app_error", + "translation": "Ja hi ha un compte associat a aquesta adreça de correu amb un mètode d'inici de sessió diferent de {{.Service}}. Inicia sessió mitjançant {{.Auth}}." + }, + { + "id": "api.user.create_email_token.error", + "translation": "No s'han pogut crear el testimoni de dades per a la verificació del correu" + }, + { + "id": "api.user.complete_switch_with_oauth.parse.app_error", + "translation": "No s'ha pogut obtenir les dades d'autorització de l'objecte de {{.Service}}" + }, + { + "id": "api.user.complete_switch_with_oauth.blank_email.app_error", + "translation": "Correu en blanc" + }, + { + "id": "api.user.check_user_password.invalid.app_error", + "translation": "L'inici de sessió ha fallat perquè la contrasenya no és vàlida" + }, + { + "id": "api.user.check_user_mfa.bad_code.app_error", + "translation": "Testimoni MFA no vàlid." + }, + { + "id": "api.user.check_user_login_attempts.too_many.app_error", + "translation": "El teu compte està bloquejat per molts intents de contrasenya fallits. Restableix la contrasenya." + }, + { + "id": "api.user.authorize_oauth_user.unsupported.app_error", + "translation": "Proveïdor de serveis OAuth no compatible" + }, + { + "id": "api.user.authorize_oauth_user.token_failed.app_error", + "translation": "La sol·licitud del testimoni ha fallat" + }, + { + "id": "api.user.authorize_oauth_user.service.app_error", + "translation": "La sol·licitar del testimoni a {{.Service}} ha fallat" + }, + { + "id": "api.user.authorize_oauth_user.response.app_error", + "translation": "S'ha rebut una resposta no vàlida del proveïdor de serveis d'OAuth" + }, + { + "id": "api.user.authorize_oauth_user.missing.app_error", + "translation": "Falta el testimoni d'accés" + }, + { + "id": "api.user.authorize_oauth_user.invalid_state.app_error", + "translation": "Estat no vàlid" + }, + { + "id": "api.user.authorize_oauth_user.bad_token.app_error", + "translation": "Tipus de testimoni incorrecte" + }, + { + "id": "api.user.authorize_oauth_user.bad_response.app_error", + "translation": "Resposta incorrecta a la sol·licitud del testimoni" + }, + { + "id": "api.user.add_direct_channels_and_forget.failed.error", + "translation": "No s'ha pogut afegir preferències de canal directe per a l'usuari user_id={{.UserId}}, team_id={{.TeamId}}, err={{.Error}}" + }, + { + "id": "api.user.activate_mfa.email_and_ldap_only.app_error", + "translation": "MFA no està disponible per a aquest tipus de compte" + }, + { + "id": "api.templates.welcome_subject", + "translation": "[{{ .SiteName }}] T'has unit a {{ .ServerURL }}" + }, + { + "id": "api.templates.welcome_body.title", + "translation": "T'has unit a {{ .ServerURL }}" + }, + { + "id": "api.templates.welcome_body.info", + "translation": "Verifica la teva adreça de correu fent clic a continuació." + }, + { + "id": "api.templates.welcome_body.button", + "translation": "Verifica el correu" + }, + { + "id": "api.templates.welcome_body.app_download_info", + "translation": "Per obtenir la millor experiència, baixa les aplicacions per a PC, Mac, iOS i Android des de:" + }, + { + "id": "api.templates.verify_subject", + "translation": "[{{ .SiteName }}] Verificació de correu" + }, + { + "id": "api.templates.verify_body.title", + "translation": "T'has unit a {{ .ServerURL}}" + }, + { + "id": "api.templates.verify_body.info", + "translation": "Verifica la teva adreça de correu fent clic a continuació." + }, + { + "id": "api.templates.verify_body.button", + "translation": "Verifica el correu" + }, + { + "id": "api.templates.username_change_subject", + "translation": "[{{ .SiteName }}] El teu nom d'usuari ha canviat" + }, + { + "id": "api.templates.username_change_body.title", + "translation": "Has actualitzat el teu nom d'usuari" + }, + { + "id": "api.templates.username_change_body.info", + "translation": "El teu nom d'usuari per a {{.TeamDisplayName}} s'ha modificat a {{.NewUsername}}." + }, + { + "id": "api.templates.user_access_token_subject", + "translation": "[{{ .SiteName}}] Testimoni d'accés personal afegit al teu compte" + }, + { + "id": "api.templates.user_access_token_body.title", + "translation": "S'ha afegit el testimoni d'accés personal al teu compte" + }, + { + "id": "api.templates.user_access_token_body.info", + "translation": "S'ha afegit un testimoni d'accés personal al teu compte a {{ .SiteURL}}. Es poden utilitzar per accedir a {{.SiteName}} amb el teu compte." + }, + { + "id": "api.templates.signin_change_email.subject", + "translation": "[{{ .SiteName}}] El mètode d'inici de sessió s'ha actualitzat" + }, + { + "id": "api.templates.signin_change_email.body.title", + "translation": "Has actualitzat el mètode d'inici de sessió" + }, + { + "id": "api.templates.signin_change_email.body.method_email", + "translation": "correu i contrasenya" + }, + { + "id": "api.templates.signin_change_email.body.info", + "translation": "Heu actualitzat el mètode d'inici de sessió a {{ .SiteName}} via {{.Method}}." + }, + { + "id": "api.templates.reset_subject", + "translation": "[{(.SiteName}}] Restableix la contrasenya" + }, + { + "id": "api.templates.reset_body.title", + "translation": "Has sol·licitat un restabliment de contrasenya" + }, + { + "id": "api.templates.reset_body.button", + "translation": "Restableix la contrasenya" + }, + { + "id": "api.templates.post_body.button", + "translation": "Vés a la publicació" + }, + { + "id": "api.templates.password_change_subject", + "translation": "[{{ .SiteName}}] La contrasenya s'ha actualitzat" + }, + { + "id": "api.templates.password_change_body.title", + "translation": "La contrasenya s'ha actualitzat" + }, + { + "id": "api.templates.password_change_body.info", + "translation": "La teva contrasenya s'ha actualitzat per a {{.TeamDisplayName}} a {{ .TeamURL}} vía {{.Method}}." + }, + { + "id": "api.templates.mfa_deactivated_body.title", + "translation": "S'ha eliminat l'autenticació de diversos factors" + }, + { + "id": "api.templates.mfa_deactivated_body.info", + "translation": "S'ha eliminat l'autenticació de diversos factors del compte a {{ .SiteURL}}." + }, + { + "id": "api.templates.mfa_change_subject", + "translation": "[{{ .SiteName}}] El teu MFA s'ha actualitzat" + }, + { + "id": "api.templates.mfa_activated_body.title", + "translation": "S'ha afegit l'autenticació de diversos factors" + }, + { + "id": "api.templates.mfa_activated_body.info", + "translation": "S'ha afegit l'autenticació de diversos factors al teu compte a {{ .SiteURL}}." + }, + { + "id": "api.templates.invite_subject", + "translation": "[{{ .SiteName}}] {{ .SenderName}} t'ha convidat a unir-te a l'equip {{.TeamDisplayName}}" + }, + { + "id": "api.templates.invite_guest_subject", + "translation": "[{{ .SiteName}}] {{ .SenderName}} t'ha convidat a unir-te a l'equip {{.TeamDisplayName}}" + }, + { + "id": "api.templates.invite_body.title", + "translation": "Has estat convidat" + }, + { + "id": "api.templates.invite_body.button", + "translation": "Uneix-me a l'equip" + }, + { + "id": "api.templates.email_warning", + "translation": "Si no has fet aquest canvi, contacta amb l'administrador del sistema." + }, + { + "id": "api.templates.email_organization", + "translation": "Enviat per " + }, + { + "id": "api.templates.email_info3", + "translation": "L'equip {{.SiteName}}" + }, + { + "id": "api.templates.email_info2", + "translation": "Els millors desitjos," + }, + { + "id": "api.templates.email_info1", + "translation": "Si tens alguna pregunta, envia'ns un correu en qualsevol moment: " + }, + { + "id": "api.templates.email_footer", + "translation": "Per canviar les preferències de notificació, inicia sessió al lloc del teu equip i ves a Configuració del compte > Notificacions." + }, + { + "id": "api.templates.email_change_verify_subject", + "translation": "[{{ .SiteName}}] Verifica l'adreça de correu nova" + }, + { + "id": "api.templates.email_change_verify_body.title", + "translation": "Has actualitzat el teu correu" + }, + { + "id": "api.templates.email_change_verify_body.info", + "translation": "Per acabar d'actualitzar la teva adreça de correu per a {{.TeamDisplayName}}, fes clic a l'enllaç següent per confirmar que aquesta és l'adreça correcta." + }, + { + "id": "api.templates.email_change_verify_body.button", + "translation": "Verifica el correu" + }, + { + "id": "api.templates.email_change_subject", + "translation": "[{{ .SiteName}}] La teva adreça de correu ha canviat" + }, + { + "id": "api.templates.email_change_body.title", + "translation": "Has actualitzat el teu correu" + }, + { + "id": "api.templates.email_change_body.info", + "translation": "La teva adreça de correu per a {{.TeamDisplayName}} s'ha modificat a {{.NewEmail}}." + }, + { + "id": "api.templates.deactivate_subject", + "translation": "[{(.SiteName}}] El teu compte a {{.ServerURL}} s'ha desactivat" + }, + { + "id": "api.templates.deactivate_body.warning", + "translation": "Si no has iniciat aquest canvi o vols reiniciar el teu compte, contacta amb el teu administrador del sistema." + }, + { + "id": "api.templates.deactivate_body.title", + "translation": "El teu compte s'ha desactivat a {{.ServerURL}}" + }, + { + "id": "api.templates.deactivate_body.info", + "translation": "Has desactivat el teu compte a {{.SiteURL}}." + }, + { + "id": "api.team.update_team_scheme.scheme_scope.error", + "translation": "No es pot establir l'esquema a l'equip perquè l'esquema propionat no és un esquema d'equip." + }, + { + "id": "api.team.update_team_scheme.license.error", + "translation": "La teva llicència no és compatible amb l'actualització de l'esquema d'un equip" + }, + { + "id": "api.team.update_team_member_roles.guest_and_user.app_error", + "translation": "Actualització del membre de l'equip invàlida: un usuari ha de ser convidat o usuari, però no els dos a la vegada." + }, + { + "id": "api.team.update_restricted_domains.mismatch.app_error", + "translation": "La restricció de l'equip a {{.Domain}} no està permès per la configuració del sistema. Contacta amb l'administrador del sistema." + }, + { + "id": "api.team.update_member_roles.not_a_member", + "translation": "L'usuari especificat no és membre de l'equip especificat." + }, + { + "id": "api.team.team_icon.update.app_error", + "translation": "S'ha produït un error en actualitzar la icona de l'equip" + }, + { + "id": "api.team.set_team_icon.write_file.app_error", + "translation": "No s'ha pogut desar la icona de l'equip" + }, + { + "id": "api.team.set_team_icon.too_large.app_error", + "translation": "No es pot penjar la icona de l'equip. El fitxer és massa gran." + }, + { + "id": "api.team.set_team_icon.storage.app_error", + "translation": "No es pot penjar la icona de l'equip. L'emmagatzematge d'imatges no està configurat." + }, + { + "id": "api.team.set_team_icon.parse.app_error", + "translation": "No s'ha pogut parsejar el formulari multipart" + }, + { + "id": "api.team.set_team_icon.open.app_error", + "translation": "No s'ha pogut obrir el fitxer d'imatge" + }, + { + "id": "api.team.set_team_icon.no_file.app_error", + "translation": "Cap fitxer sota «imatge» a la petició" + }, + { + "id": "api.team.set_team_icon.get_team.app_error", + "translation": "S'ha produït un error en obtenir l'equip" + }, + { + "id": "api.team.set_team_icon.encode.app_error", + "translation": "No s'ha pogut codificar la icona de l'equip" + }, + { + "id": "api.team.set_team_icon.decode.app_error", + "translation": "No s'ha pogut descodificar la icona de l'equip" + }, + { + "id": "api.team.set_team_icon.array.app_error", + "translation": "Matriu buida sota 'image' a la petició" + }, + { + "id": "api.team.search_teams.pagination_not_implemented.public_team_search", + "translation": "La paginació no està implementada per a la cerca d'equips en mode privat" + }, + { + "id": "api.team.search_teams.pagination_not_implemented.private_team_search", + "translation": "La paginació no està implementada per a la cerca d'equips en mode privat" + }, + { + "id": "api.team.remove_user_from_team.removed", + "translation": "%v eliminat de l'equip." + }, + { + "id": "api.team.remove_user_from_team.missing.app_error", + "translation": "L'usuari no sembla formar part d'aquest equip." + }, + { + "id": "api.team.remove_team_icon.get_team.app_error", + "translation": "S'ha produït un error en obtenir l'equip" + }, + { + "id": "api.team.remove_member.group_constrained.app_error", + "translation": "No es pot eliminar un usuari d’un equip amb restriccions de grup." + }, + { + "id": "api.team.move_channel.success", + "translation": "Aquest canal s'ha mogut a aquest equip des de %v." + }, + { + "id": "api.team.move_channel.post.error", + "translation": "No s'ha pogut publicar el missatge que el canal s'ha mogut." + }, + { + "id": "api.team.leave.left", + "translation": "%v ha deixat l'equip." + }, + { + "id": "api.team.join_team.post_and_forget", + "translation": "%v s'ha unit a l'equip." + }, + { + "id": "api.team.is_team_creation_allowed.disabled.app_error", + "translation": "S'ha desactivat la creació d'equips. Pregunteu a l'administrador del sistema per obtenir més detalls." + }, + { + "id": "api.team.invite_members.no_one.app_error", + "translation": "Ningú a convidar." + }, + { + "id": "api.team.invite_members.invalid_email.app_error", + "translation": "Les següents adreces de correu no pertanyen a un domini acceptat: {{.Addresses}}. Contacta amb l'administrador del sistema per obtenir més detalls." + }, + { + "id": "model.config.is_valid.image_proxy_type.app_error", + "translation": "El tipus de servidor intermediari d'imatge no és vàlid. Ha de ser 'local' o 'atmos/camo'." + }, + { + "id": "model.config.is_valid.file_driver.app_error", + "translation": "El nom del controlador per a la configuració del fitxer no és vàlid. Ha de ser «local» o «amazons3»" + }, + { + "id": "model.config.is_valid.email_security.app_error", + "translation": "La seguretat de la connexió no és vàlida per a la configuració del correu. Ha de ser '', 'TLS' o 'STARTTLS'" + }, + { + "id": "model.config.is_valid.email_notification_contents_type.app_error", + "translation": "La configuració del tipus de contingut per a la notificació per correu no és vàlida. Ha de ser un dels següents valors 'full' o 'generic'." + }, + { + "id": "model.config.is_valid.email_batching_interval.app_error", + "translation": "L'interval per als correus per lots en la configuració de correu no és vàlid. Ha de ser de 30 segons o més." + }, + { + "id": "model.config.is_valid.email_batching_buffer_size.app_error", + "translation": "La mida de la memòria intermèdia per als correus per lots no és vàlida per a la configuració del correu. Ha de ser zero o un número positiu." + }, + { + "id": "model.config.is_valid.elastic_search.request_timeout_seconds.app_error", + "translation": "El temps d'espera de la sol·licitud de cerca de l'Elastic ha de ser com a mínim 1 segon." + }, + { + "id": "model.config.is_valid.elastic_search.posts_aggregator_job_start_time.app_error", + "translation": "El valor «PostsAggregatorJobStartTime» d'Elasticsearch cal que sigui en format \"hh:mm\"" + }, + { + "id": "model.config.is_valid.elastic_search.live_indexing_batch_size.app_error", + "translation": "La mida del lot d'indexació en temps real d'Elasticsearch ha de ser com a mínim 1" + }, + { + "id": "model.config.is_valid.elastic_search.enable_searching.app_error", + "translation": "El valor d'Elastic Search «IndexingEnabled» ha de ser verdader quan el valor d'Elastic Search «SearchEnabled» és verdader." + }, + { + "id": "model.config.is_valid.elastic_search.enable_autocomplete.app_error", + "translation": "El valor d'Elastic Search «IndexingEnabled» ha de ser verdader quan el valor d'Elastic Search «SearchEnabled» és verdader." + }, + { + "id": "model.config.is_valid.elastic_search.connection_url.app_error", + "translation": "El valor de cerca «ConnectionUrl» de l'Elastic s'ha de proporcionar quan la indexació de cerca de l'Elastic estigui activada." + }, + { + "id": "model.config.is_valid.elastic_search.aggregate_posts_after_days.app_error", + "translation": "El valor «AggregatePostsAfterDays» de l'Elasticsearch ha de ser un número superior o igual a 1" + }, + { + "id": "model.config.is_valid.display.custom_url_schemes.app_error", + "translation": "L'esquema d'URL personalitzat {{.Scheme}} no és vàlid. Els esquemes d'URL personalitzats han de començar amb una lletra i només contenen lletres, números i guions (-)." + }, + { + "id": "model.config.is_valid.data_retention.message_retention_days_too_low.app_error", + "translation": "La retenció del missatge ha de ser d'un dia o més." + }, + { + "id": "model.config.is_valid.data_retention.file_retention_days_too_low.app_error", + "translation": "La retenció de fitxers ha de ser d'un dia o més." + }, + { + "id": "model.config.is_valid.data_retention.deletion_job_start_time.app_error", + "translation": "L'hora d'inici del treball de retenció de dades ha de ser en format 24 hores seguint la forma HH:MM." + }, + { + "id": "model.config.is_valid.cluster_email_batching.app_error", + "translation": "No es pot activar l'enviament per correu per lots quan el clúster està activat." + }, + { + "id": "model.config.is_valid.bleve_search.enable_searching.app_error", + "translation": "El valor d'Elastic Search «IndexingEnabled» ha de ser verdader quan el valor d'Elastic Search «SearchEnabled» és verdader." + }, + { + "id": "model.config.is_valid.bleve_search.enable_autocomplete.app_error", + "translation": "El valor d'Elastic Search «IndexingEnabled» ha de ser verdader quan el valor d'Elastic Search «SearchEnabled» és verdader." + }, + { + "id": "model.config.is_valid.atmos_camo_image_proxy_url.app_error", + "translation": "El valor «RemoteImageProxyURL» no és vàlid per a l'atmos/camo. Heu de configurar-lo a la vostra clau compartida." + }, + { + "id": "model.config.is_valid.atmos_camo_image_proxy_options.app_error", + "translation": "El valor «RemoteImageProxyOptions» no és vàlid per a l'atmos/camo. Heu de configurar-lo a la vostra clau compartida." + }, + { + "id": "model.config.is_valid.allow_cookies_for_subdomains.app_error", + "translation": "En permetre les galetes per als subdominis, cal que configureu «SiteURL»." + }, + { + "id": "model.compliance.is_valid.start_end_at.app_error", + "translation": "«To» ha de ser més gran que «From»" + }, + { + "id": "model.compliance.is_valid.start_at.app_error", + "translation": "«From» ha de ser un temps vàlid" + }, + { + "id": "model.compliance.is_valid.id.app_error", + "translation": "ID no vàlid" + }, + { + "id": "model.compliance.is_valid.end_at.app_error", + "translation": "«To» ha de ser un temps vàlid" + }, + { + "id": "model.compliance.is_valid.desc.app_error", + "translation": "Descripció no vàlida" + }, + { + "id": "model.compliance.is_valid.create_at.app_error", + "translation": "«Create at» ha de ser un temps vàlid" + }, + { + "id": "model.command_hook.user_id.app_error", + "translation": "ID d'usuari no vàlid" + }, + { + "id": "model.command_hook.root_id.app_error", + "translation": "ID d'arrel no vàlid" + }, + { + "id": "model.command_hook.id.app_error", + "translation": "Ordre amb «hook id» no vàlid" + }, + { + "id": "model.command_hook.create_at.app_error", + "translation": "«Create at» ha de ser un temps vàlid" + }, + { + "id": "model.command_hook.command_id.app_error", + "translation": "ID d'ordre no vàlid" + }, + { + "id": "model.command_hook.channel_id.app_error", + "translation": "ID del canal no vàlid" + }, + { + "id": "model.command.is_valid.user_id.app_error", + "translation": "ID d'usuari no vàlid" + }, + { + "id": "model.command.is_valid.url_http.app_error", + "translation": "URL no vàlid. Ha de ser un URL vàlid i començar amb http:// o https://" + }, + { + "id": "model.command.is_valid.url.app_error", + "translation": "URL no vàlid" + }, + { + "id": "model.command.is_valid.update_at.app_error", + "translation": "«Update at» ha de ser un temps vàlid" + }, + { + "id": "model.command.is_valid.trigger.app_error", + "translation": "Disparador no vàlid" + }, + { + "id": "model.command.is_valid.token.app_error", + "translation": "Testimoni no vàlid." + }, + { + "id": "model.command.is_valid.team_id.app_error", + "translation": "ID d'equip no vàlid" + }, + { + "id": "model.command.is_valid.method.app_error", + "translation": "Mètode no vàlid" + }, + { + "id": "model.command.is_valid.id.app_error", + "translation": "ID no vàlid" + }, + { + "id": "model.command.is_valid.display_name.app_error", + "translation": "Títol no vàlid" + }, + { + "id": "model.command.is_valid.description.app_error", + "translation": "Descripció no vàlida" + }, + { + "id": "model.command.is_valid.create_at.app_error", + "translation": "«Create at» ha de ser un temps vàlid" + }, + { + "id": "model.cluster.is_valid.type.app_error", + "translation": "«Type» s'ha de configurar" + }, + { + "id": "model.cluster.is_valid.name.app_error", + "translation": "«ClusterName» s'ha de configurar" + }, + { + "id": "model.cluster.is_valid.last_ping_at.app_error", + "translation": "«LastPingAt» s'ha de configurar" + }, + { + "id": "model.cluster.is_valid.id.app_error", + "translation": "ID no vàlid" + }, + { + "id": "model.cluster.is_valid.hostname.app_error", + "translation": "«Hostname» s'ha de configurar" + }, + { + "id": "model.cluster.is_valid.create_at.app_error", + "translation": "«CreateAt» s'ha de configurar" + }, + { + "id": "model.channel_member.is_valid.user_id.app_error", + "translation": "ID d'usuari no vàlid" + }, + { + "id": "model.channel_member.is_valid.unread_level.app_error", + "translation": "Nivell de marca per no llegits no vàlid" + }, + { + "id": "model.channel_member.is_valid.push_level.app_error", + "translation": "Nivell de notificació de tramesa automàtica no vàlid" + }, + { + "id": "model.channel_member.is_valid.notify_level.app_error", + "translation": "Nivell de notificació no vàlid" + }, + { + "id": "model.channel_member.is_valid.ignore_channel_mentions_value.app_error", + "translation": "Valor no vàlid per ignorar mencions de canals" + }, + { + "id": "model.channel_member.is_valid.email_value.app_error", + "translation": "Valor de notificació de correu no vàlid" + }, + { + "id": "model.channel_member.is_valid.channel_id.app_error", + "translation": "ID del canal no vàlid" + }, + { + "id": "model.channel.is_valid.update_at.app_error", + "translation": "«Update at» ha de ser un temps vàlid" + }, + { + "id": "model.channel.is_valid.type.app_error", + "translation": "Tipus no vàlid" + }, + { + "id": "model.channel.is_valid.purpose.app_error", + "translation": "Propòsit no vàlid" + }, + { + "id": "model.channel.is_valid.id.app_error", + "translation": "ID no vàlid" + }, + { + "id": "model.channel.is_valid.header.app_error", + "translation": "Encapçalament no vàlid" + }, + { + "id": "model.channel.is_valid.display_name.app_error", + "translation": "Nom de visualització no vàlid." + }, + { + "id": "model.channel.is_valid.creator_id.app_error", + "translation": "ID del creador no vàlid" + }, + { + "id": "model.channel.is_valid.create_at.app_error", + "translation": "«Create at» ha de ser un temps vàlid" + }, + { + "id": "model.bot.is_valid.username.app_error", + "translation": "Nom d'usuari no vàlid." + }, + { + "id": "model.bot.is_valid.user_id.app_error", + "translation": "ID d'usuari no vàlid" + }, + { + "id": "model.bot.is_valid.update_at.app_error", + "translation": "«update at» no vàlid" + }, + { + "id": "model.bot.is_valid.description.app_error", + "translation": "Descripció no vàlida" + }, + { + "id": "model.bot.is_valid.creator_id.app_error", + "translation": "ID del creador no vàlid" + }, + { + "id": "model.bot.is_valid.create_at.app_error", + "translation": "«create at» no vàlid" + }, + { + "id": "model.authorize.is_valid.user_id.app_error", + "translation": "ID d'usuari no vàlid" + }, + { + "id": "model.authorize.is_valid.state.app_error", + "translation": "Estat no vàlid" + }, + { + "id": "model.authorize.is_valid.scope.app_error", + "translation": "Abast no vàlid" + }, + { + "id": "model.authorize.is_valid.response_type.app_error", + "translation": "Tipus de resposta no vàlida" + }, + { + "id": "model.authorize.is_valid.redirect_uri.app_error", + "translation": "URI de redireccionament no vàlid" + }, + { + "id": "model.authorize.is_valid.expires.app_error", + "translation": "S'ha de configurar «Expires in»" + }, + { + "id": "model.authorize.is_valid.create_at.app_error", + "translation": "«Create at» ha de ser un temps vàlid" + }, + { + "id": "model.authorize.is_valid.client_id.app_error", + "translation": "ID de client no vàlid" + }, + { + "id": "model.authorize.is_valid.auth_code.app_error", + "translation": "Codi d'autorització no vàlid" + }, + { + "id": "model.access.is_valid.user_id.app_error", + "translation": "ID d'usuari no vàlid" + }, + { + "id": "model.access.is_valid.refresh_token.app_error", + "translation": "Testimoni de refresc no vàlid" + }, + { + "id": "model.access.is_valid.redirect_uri.app_error", + "translation": "URI de redireccionament no vàlid" + }, + { + "id": "model.access.is_valid.client_id.app_error", + "translation": "ID de client no vàlid" + }, + { + "id": "model.access.is_valid.access_token.app_error", + "translation": "Testimoni d'accés no vàlid" + }, + { + "id": "migrations.worker.run_migration.unknown_key", + "translation": "No es pot executar el treball de migració perquè la clau de migració és desconeguda." + }, + { + "id": "migrations.worker.run_advanced_permissions_phase_2_migration.invalid_progress", + "translation": "La migració ha fallat a causa de dades de progrés no vàlides." + }, + { + "id": "mfa.validate_token.authenticate.app_error", + "translation": "Testimoni MFA no vàlid." + }, + { + "id": "mfa.mfa_disabled.app_error", + "translation": "L'autenticació multifactor s'ha desactivat en aquest servidor." + }, + { + "id": "mfa.generate_qr_code.create_code.app_error", + "translation": "S'ha produït un error en generar el codi QR" + }, + { + "id": "mfa.activate.bad_token.app_error", + "translation": "Testimoni MFA no vàlid." + }, + { + "id": "mattermost.bulletin.subject", + "translation": "Butlletí de seguretat Mattermost" + }, + { + "id": "manaultesting.test_autolink.unable.app_error", + "translation": "No s'han pogut obtenir els canals" + }, + { + "id": "manaultesting.manual_test.parse.app_error", + "translation": "No es pot analitzar l'URL" + }, + { + "id": "jobs.set_job_error.update.error", + "translation": "No s'ha pogut establir l'estat del treball a error" + }, + { + "id": "jobs.request_cancellation.status.error", + "translation": "No s'ha pogut sol·licitar la cancel·lació per a un treball que no es troba en un estat cancel·lable." + }, + { + "id": "interactive_message.generate_trigger_id.signing_failed", + "translation": "No s'ha pogut signar l'ID del disparador generat per al diàleg interactiu." + }, + { + "id": "interactive_message.decode_trigger_id.verify_signature_failed", + "translation": "La verificació de la signatura de l'ID del desencadenant del diàleg interactiu ha fallat." + }, + { + "id": "interactive_message.decode_trigger_id.signature_decode_failed", + "translation": "No s'ha pogut descodificar la signatura base64 de l'ID del disparador del diàleg interactiu." + }, + { + "id": "interactive_message.decode_trigger_id.missing_data", + "translation": "No s'ha trobat l'ID del disparador requerit per al diàleg interactiu." + }, + { + "id": "interactive_message.decode_trigger_id.expired", + "translation": "L'ID del disparador per al diàleg interactiu ha caducat. Els ID dels disparadors tenen una durada màxima de {{.Seconds}} segons." + }, + { + "id": "interactive_message.decode_trigger_id.base64_decode_failed_signature", + "translation": "No s'ha pogut descodificar la signatura base64 de l'ID del disparador del diàleg interactiu." + }, + { + "id": "interactive_message.decode_trigger_id.base64_decode_failed", + "translation": "No s'ha pogut realitzar la descodificació base64 per a l'ID del disparador del diàleg interactiu." + }, + { + "id": "ent.saml.service_disable.app_error", + "translation": "SAML 2.0 no està configurat o no és compatible en aquest servidor." + }, + { + "id": "ent.saml.metadata.app_error", + "translation": "S'ha produït un error en crear les metadades del proveïdor de serveis." + }, + { + "id": "ent.saml.license_disable.app_error", + "translation": "La teva llicència no és compatible amb l'autenticació SAML." + }, + { + "id": "ent.saml.do_login.validate.app_error", + "translation": "S'ha produït un error en validar la resposta del proveïdor d'identitat. Contacta amb l'administrador del sistema." + }, + { + "id": "ent.saml.do_login.parse.app_error", + "translation": "S'ha produït un error durant l'anàlisi de la resposta del proveïdor d'identitat. Contacta amb l'administrador del sistema." + }, + { + "id": "ent.saml.do_login.empty_response.app_error", + "translation": "Hem rebut una resposta buida del proveïdor d'identitat" + }, + { + "id": "ent.saml.do_login.decrypt.app_error", + "translation": "L'inici de sessió SAML no s'ha pogut completar perquè s'ha produït un error mentre es desxifra la resposta del proveïdor d'identitat. Contacta amb l'administrador del sistema." + }, + { + "id": "ent.saml.configure.not_encrypted_response.app_error", + "translation": "L'inici de sessió SAML no s'ha pogut completar perquè la resposta del proveïdor d'identitat no està xifrada. Contacta amb l'administrador del sistema." + }, + { + "id": "ent.saml.configure.load_private_key.app_error", + "translation": "L'inici de sessió SAML no s'ha pogut completar perquè no s'ha trobat la clau privada del proveïdor de serveis. Contacta amb l'administrador del sistema." + }, + { + "id": "ent.saml.configure.load_idp_cert.app_error", + "translation": "No s'ha trobat el fitxer de certificat públic del proveïdor d'identitat. Contacta amb l'administrador del sistema." + }, + { + "id": "ent.saml.configure.encryption_not_enabled.app_error", + "translation": "L'inici de sessió SAML no s'ha pogut completar perquè el xifratge no està activat. Contacta amb l'administrador del sistema." + }, + { + "id": "ent.saml.build_request.encoding.app_error", + "translation": "S'ha produït un error durant la codificació de la sol·licitud per al proveïdor d'identitat. Contacta amb l'administrador del sistema." + }, + { + "id": "ent.saml.build_request.app_error", + "translation": "S'ha produït un error en iniciar la sol·licitud al proveïdor d'identitat. Contacta amb l'administrador del sistema." + }, + { + "id": "ent.saml.attribute.app_error", + "translation": "L'inici de sessió SAML no s'ha pogut completar perquè un dels atributs és incorrecte. Contacta amb l'administrador del sistema." + }, + { + "id": "ent.migration.migratetosaml.username_already_used_by_other_user", + "translation": "El nom d'usuari ja utilitza per un altre usuari de Mattermost." + }, + { + "id": "ent.migration.migratetosaml.user_not_found_in_users_mapping_file", + "translation": "L'usuari no s'ha trobat al fitxer dels usuaris." + }, + { + "id": "ent.migration.migratetosaml.email_already_used_by_other_user", + "translation": "El correu ja s'utilitza per un altre usuari SAML." + }, + { + "id": "ent.migration.migratetoldap.user_not_found", + "translation": "No es pot trobar l'usuari al servidor AD/LDAP: " + }, + { + "id": "ent.migration.migratetoldap.duplicate_field", + "translation": "No es poden migrar els usuaris d'AD/LDAP amb un camp especificat. S'ha detectat una entrada duplicada. Elimina tots els duplicats i torna-ho a provar." + }, + { + "id": "ent.message_export.global_relay_export.deliver.unable_to_open_zip_file_data.app_error", + "translation": "No es pot obrir el fitxer temporal d'exportació" + }, + { + "id": "ent.message_export.global_relay_export.deliver.unable_to_open_email_file.app_error", + "translation": "No es pot obtenir un correu del fitxer temporal" + }, + { + "id": "ent.message_export.global_relay_export.deliver.unable_to_get_file_info.app_error", + "translation": "No es pot obtenir la informació del fitxer temporal d'exportació" + }, + { + "id": "ent.message_export.global_relay_export.deliver.to_address.app_error", + "translation": "No es pot configurar l'adreça Per a del correu." + }, + { + "id": "ent.message_export.global_relay_export.deliver.parse_mail.app_error", + "translation": "No es pot llegir la informació del correu." + }, + { + "id": "ent.message_export.global_relay_export.deliver.msg_data.app_error", + "translation": "No es pot escriure el missatge del correu." + }, + { + "id": "ent.message_export.global_relay_export.deliver.msg.app_error", + "translation": "No es pot configurar el missatge del correu." + }, + { + "id": "ent.message_export.global_relay_export.deliver.from_address.app_error", + "translation": "No es pot establir la direcció De per al correu." + }, + { + "id": "ent.message_export.global_relay_export.deliver.close.app_error", + "translation": "No es pot enviar el correu a Global Relay." + }, + { + "id": "ent.message_export.global_relay.generate_email.app_error", + "translation": "No es poden generar les dades del fitxer eml." + }, + { + "id": "ent.message_export.global_relay.create_file_in_zip.app_error", + "translation": "No es pot crear el fitxer eml." + }, + { + "id": "ent.message_export.global_relay.close_zip_file.app_error", + "translation": "No es pot tancar el fitxer zip." + }, + { + "id": "ent.message_export.global_relay.attach_file.app_error", + "translation": "No es pot afegir un fitxer adjunt a l'exportació de Global Relay." + }, + { + "id": "ent.ldap.validate_guest_filter.app_error", + "translation": "Filtre d'AD/LDAP no vàlid" + }, + { + "id": "ent.ldap.validate_filter.app_error", + "translation": "Filtre d'AD/LDAP no vàlid" + }, + { + "id": "ent.ldap.validate_admin_filter.app_error", + "translation": "Filtre d'AD/LDAP no vàlid" + }, + { + "id": "ent.ldap.syncronize.search_failure.app_error", + "translation": "No s'ha pogut cercar usuaris a l'AD / LDAP. Comprova si el servidor de Mattermost pot connectar-se al servidor d'AD/LDAP i torna-ho a provar." + }, + { + "id": "ent.ldap.syncronize.get_all.app_error", + "translation": "No es va poder obtenir a tots els usuaris fent servir l'AD/LDAP" + }, + { + "id": "ent.ldap.do_login.user_not_registered.app_error", + "translation": "L'usuari no està registrat al servidor AD/LDAP" + }, + { + "id": "ent.ldap.do_login.user_filtered.app_error", + "translation": "El vostre compte d'AD/LDAP no té permisos per utilitzar aquest servidor de Mattermost. Demana a l'administrador del sistema que verifiqui el filtre d'usuaris d'AD/LDAP." + }, + { + "id": "ent.ldap.do_login.unable_to_connect.app_error", + "translation": "No es pot connectar al servidor AD/LDAP" + }, + { + "id": "ent.ldap.do_login.search_ldap_server.app_error", + "translation": "No s'ha pogut cercar al servidor AD/LDAP" + }, + { + "id": "ent.ldap.do_login.matched_to_many_users.app_error", + "translation": "El nom d'usuari assignat coincideix amb diversos usuaris" + }, + { + "id": "ent.ldap.do_login.licence_disable.app_error", + "translation": "La funció d'AD/LDAP està desactivada per la llicència actual. Contacta amb l'administrador del sistema per actualitzar la teva llicència empresarial." + }, + { + "id": "ent.ldap.do_login.invalid_password.app_error", + "translation": "Contrasenya no vàlida" + }, + { + "id": "ent.ldap.do_login.bind_admin_user.app_error", + "translation": "No es pot enllaçar amb el servidor AD/LDAP. Comprova les opcions «BindUsername» i «BindPassword»." + }, + { + "id": "ent.ldap.disabled.app_error", + "translation": "L'AD/LDAP està desactivat o la llicència no és compatible amb AD/LDAP." + }, + { + "id": "ent.ldap.create_fail", + "translation": "No es pot crear l'usuari LDAP." + }, + { + "id": "ent.elasticsearch.test_config.reenter_password", + "translation": "L'URL del servidor Elasticsearch o el nom d'usuari han canviat. Torna a introduir la contrasenya de l'Elasticsearch per provar la connexió." + }, + { + "id": "ent.elasticsearch.test_config.license.error", + "translation": "La teva llicència no és compatible amb l'Elasticsearch." + }, + { + "id": "ent.elasticsearch.test_config.indexing_disabled.error", + "translation": "L'Elasticsearch està desactivat." + }, + { + "id": "ent.elasticsearch.stop.already_stopped.app_error", + "translation": "L'Elasticsearch ja s'ha aturat" + }, + { + "id": "ent.elasticsearch.start.start_bulk_processor_failed.app_error", + "translation": "No s'ha pogut iniciar el processador massiu de l'Elasticsearch" + }, + { + "id": "ent.elasticsearch.start.create_bulk_processor_failed.app_error", + "translation": "No s'ha pogut crear el processador massiu de l'Elasticsearch" + }, + { + "id": "ent.elasticsearch.start.already_started.app_error", + "translation": "L'Elasticsearch ja s'ha iniciat" + }, + { + "id": "ent.elasticsearch.search_users.unmarshall_user_failed", + "translation": "No s'ha pogut descodificar els resultats de la cerca" + }, + { + "id": "api.team.invite_members.disabled.app_error", + "translation": "Les invitacions per correu estan desactivades." + }, + { + "id": "api.team.invite_guests.channel_in_invalid_team.app_error", + "translation": "Els canals de la invitació han de pertànyer a l'equip que convida." + }, + { + "id": "api.team.invalidate_all_email_invites.app_error", + "translation": "S'ha produït un error en invalidar les invitacions per correu." + }, + { + "id": "api.team.import_team.unavailable.app_error", + "translation": "Sol·licitud incorrecta: el camp amb la mida del fitxer no està present." + }, + { + "id": "api.team.import_team.parse.app_error", + "translation": "No s'ha pogut parsejar el formulari multipart" + }, + { + "id": "api.team.import_team.open.app_error", + "translation": "No s'ha pogut obrir el fitxer" + }, + { + "id": "api.team.import_team.no_import_from.app_error", + "translation": "Sol·licitud incorrecta: el camp «importFrom» no està present." + }, + { + "id": "api.team.import_team.no_file.app_error", + "translation": "Cap fitxer sota «fitxer» a la petició" + }, + { + "id": "api.team.import_team.integer.app_error", + "translation": "La mida del fitxer no és un enter" + }, + { + "id": "api.team.import_team.array.app_error", + "translation": "Matriu buida sota «fitxer» a la petició" + }, + { + "id": "api.team.get_team_icon.read_file.app_error", + "translation": "No s'ha pogut llegir el fitxer de la icona de l'equip." + }, + { + "id": "api.team.get_team_icon.filesettings_no_driver.app_error", + "translation": "El nom del controlador per a la configuració del fitxer no és vàlid. Ha de ser «local» o «amazons3»" + }, + { + "id": "api.team.get_invite_info.not_open_team", + "translation": "La invitació no és vàlida perquè no es tracta d'un equip obert." + }, + { + "id": "api.team.get_all_teams.insufficient_permissions", + "translation": "No tens els permisos adequats per editar la capçalera del canal." + }, + { + "id": "api.team.demote_user_to_guest.license.error", + "translation": "La teva llicència no suporta comptes de convidats" + }, + { + "id": "api.team.demote_user_to_guest.disabled.error", + "translation": "Els comptes de convidats estan desactivats." + }, + { + "id": "api.team.add_user_to_team_from_invite.guest.app_error", + "translation": "Els convidats no poden unir-se a un equip mitjançant un enllaç d'invitació. Per favor, demaneu un email d'invitació a l'equip. " + }, + { + "id": "api.team.add_user_to_team.missing_parameter.app_error", + "translation": "Paràmetre necessari per afegir l'usuari a l'equip." + }, + { + "id": "api.team.add_user_to_team.added", + "translation": "%v afegit a l'equip per %v." + }, + { + "id": "api.team.add_members.user_denied", + "translation": "Aquest equip es gestiona per grups, però l'usuari no pertany a cap dels grups de l'equip." + }, + { + "id": "api.team.add_members.error", + "translation": "Error en afegir els membres de l'equip." + }, + { + "id": "api.system.id_loaded.not_available.app_error", + "translation": "Les notificacions push carregades per ID no estan configurades ni admeses en aquest servidor." + }, + { + "id": "api.status.user_not_found.app_error", + "translation": "Usuari no trobat" + }, + { + "id": "api.slackimport.slack_import.zip.file_too_large", + "translation": "{{.Filename}} a l'arxiu zip massa gran per processar-lo per a la importació de Slack\r\n" + }, + { + "id": "api.slackimport.slack_import.zip.app_error", + "translation": "No s'ha pogut obrir el fitxer zip d'exportació de Slack.\r\n" + }, + { + "id": "api.slackimport.slack_import.team_fail", + "translation": "No s'ha pogut obtenir l'equip al qual s'ha d'importar.\r\n" + }, + { + "id": "api.slackimport.slack_import.open.app_error", + "translation": "No s'ha pogut obrir el fitxer: {{.Filename}}.\r\n" + }, + { + "id": "api.slackimport.slack_import.notes", + "translation": "\r\nNotes:\r\n" + }, + { + "id": "api.slackimport.slack_import.note3", + "translation": "- Es poden trobar errors addicionals als registres del servidor.\r\n" + }, + { + "id": "api.slackimport.slack_import.note2", + "translation": "- Actualment, els missatges de Slack bot no són compatibles.\r\n" + }, + { + "id": "api.slackimport.slack_import.note1", + "translation": "- Pot ser que alguns missatges no s'hagin importat perquè aquest importador no és compatible.\r\n" + }, + { + "id": "api.slackimport.slack_import.log", + "translation": "Registre d'importació de Slack a Mattermost\r\n" + }, + { + "id": "api.slackimport.slack_add_users.unable_import", + "translation": "No s'ha pogut importar l'usuari de Slack: {{.Username}}.\r\n" + }, + { + "id": "api.slackimport.slack_add_users.missing_email_address", + "translation": "L'usuari {{.Username}} no té una adreça de correu a l'exportació de Slack. S'ha utilitzat {{.Email}} com a marcador de posició. L'usuari haurà d'actualitzar la seva adreça de correu quan es connecti al sistema.\r\n" + }, + { + "id": "api.slackimport.slack_add_users.merge_existing_failed", + "translation": "L'usuari de Slack s'ha fusionat amb un usuari Mattermost existent amb el correu {{.Email}} i el nom d'usuari {{.Username}} corresponents, però l'usuari no s'ha pogut afegir al teu equip.\r\n" + }, + { + "id": "api.slackimport.slack_add_users.merge_existing", + "translation": "L'usuari de Slack s'ha fusionat amb un usuari de Mattermost existent amb el correu {{.Email}} i el nom d'usuari {{.Username}} corresponents.\r\n" + }, + { + "id": "api.slackimport.slack_add_users.email_pwd", + "translation": "S'ha importat l'usuari de Slack amb el correu {{.Email}} i la contrasenya {{.Password}}.\r\n" + }, + { + "id": "api.slackimport.slack_add_users.created", + "translation": "\r\nUsuaris creats:\r\n" + }, + { + "id": "api.slackimport.slack_add_channels.merge", + "translation": "El canal de Slack {{.DisplayName}} ja existeix com un canal actiu de Mattermost. S'han fusionat tots dos canals.\r\n" + }, + { + "id": "api.slackimport.slack_add_channels.import_failed", + "translation": "No s'ha pogut importar el canal de Slack {{.DisplayName}}.\r\n" + }, + { + "id": "api.slackimport.slack_add_channels.failed_to_add_user", + "translation": "No s'ha pogut afegir l'usuari de Slack {{.Username}} al canal.\r\n" + }, + { + "id": "api.slackimport.slack_add_channels.added", + "translation": "\r\nCanals afegits:\r\n" + }, + { + "id": "api.slackimport.slack_add_bot_user.unable_import", + "translation": "No s'ha pogut importar l'usuari d'integració/Slack Bot {{.Username}}.\r\n" + }, + { + "id": "api.slackimport.slack_add_bot_user.email_pwd", + "translation": "S'ha importat l'usuari d'integració/Slack Bot amb el correu {{.Email}} i la contrasenya {{.Password}}.\r\n" + }, + { + "id": "api.server.start_server.starting.critical", + "translation": "S'ha produït un error en iniciar el servidor, err: %v" + }, + { + "id": "api.server.start_server.rate_limiting_rate_limiter", + "translation": "No s'ha pogut iniciar el límit de velocitat." + }, + { + "id": "api.server.start_server.rate_limiting_memory_store", + "translation": "No s'ha pogut iniciar el magatzem de memòria de limitació de velocitat. Comprova «MemoryStoreSize» a la configuració." + }, + { + "id": "api.server.start_server.forward80to443.enabled_but_listening_on_wrong_port", + "translation": "No s'ha pogut reenviar del port 80 al port 443 mentre s'escolta al port %s: desactiva «Forward80To443» si utilitzes un servidor intermediari" + }, + { + "id": "api.server.start_server.forward80to443.disabled_while_using_lets_encrypt", + "translation": "Has d'activar «Forward80To443» quan feu servir «LetsEncrypt»" + }, + { + "id": "api.post.send_notifications_and_forget.push_general_message", + "translation": " ha publicat un missatge." + }, + { + "id": "api.post.send_notifications_and_forget.push_explicit_mention", + "translation": " t'ha mencionat." + }, + { + "id": "api.post.send_notification_and_forget.push_comment_on_thread", + "translation": " ha comentat un fil en què vas participar." + }, + { + "id": "api.post.send_notification_and_forget.push_comment_on_post", + "translation": " ha comentat la teva publicació." + }, + { + "id": "api.post.send_notification_and_forget.push_channel_mention", + "translation": " ha notificat el canal." + }, + { + "id": "api.post.patch_post.can_not_update_post_in_deleted.error", + "translation": "No es pot actualitzar una publicació en un canal suprimit." + }, + { + "id": "api.post.link_preview_disabled.app_error", + "translation": "L'administrador del sistema ha desactivat les vistes prèvies dels enllaços." + }, + { + "id": "api.post.get_message_for_notification.images_sent", + "translation": { + "one": "{{.Count}} imatge enviada: {{.Filenames}}", + "other": "{{.Count}} imatges enviades: {{.Filenames}}" + } + }, + { + "id": "api.post.get_message_for_notification.files_sent", + "translation": { + "one": "{{.Count}} fitxer enviat: {{.Filenames}}", + "other": "{{.Count}} fitxers enviats: {{.Filenames}}" + } + }, + { + "id": "api.post.do_action.action_integration.app_error", + "translation": "Error d'integració de l'acció" + }, + { + "id": "api.post.do_action.action_id.app_error", + "translation": "ID d'acció no vàlida" + }, + { + "id": "api.post.disabled_here", + "translation": "@here s'ha desactivat perquè el canal té més de {{.Users}} usuaris." + }, + { + "id": "api.post.disabled_channel", + "translation": "@channel s'ha desactivat perquè el canal té més de {{.Users}} usuaris." + }, + { + "id": "api.post.disabled_all", + "translation": "@all s'ha desactivat perquè el canal té més de {{.Users}} usuaris." + }, + { + "id": "api.post.delete_post.can_not_delete_post_in_deleted.error", + "translation": "No es pot suprimir una publicació en un canal suprimit." + }, + { + "id": "api.post.deduplicate_create_post.pending", + "translation": "S'ha rebutjat la publicació perquè un altre client fa la mateixa sol·licitud." + }, + { + "id": "api.post.deduplicate_create_post.failed_to_get", + "translation": "No s'ha pogut obtenir la publicació original després de duplicar un client que repeteix la mateixa sol·licitud." + }, + { + "id": "api.post.create_webhook_post.creating.app_error", + "translation": "Error en crear la publicació" + }, + { + "id": "api.post.create_post.root_id.app_error", + "translation": "Paràmetre «RootId» no vàlid" + }, + { + "id": "api.post.create_post.channel_root_id.app_error", + "translation": "«ChannelId» no vàlid per al paràmetre «RootId»" + }, + { + "id": "api.post.create_post.can_not_post_to_deleted.error", + "translation": "No es pot publicar al canal suprimit." + }, + { + "id": "api.post.check_for_out_of_channel_mentions.message.one", + "translation": "A @{{.Usernames}} no se li ha notificat aquesta menció perquè no està al canal." + }, + { + "id": "api.post.check_for_out_of_channel_mentions.message.multiple", + "translation": "A @{{.Usernames}} i a @{{.LastUsername}} no se'ls ha notificat aquesta menció perquè no estan al canal." + }, + { + "id": "api.post.check_for_out_of_channel_groups_mentions.message.one", + "translation": "A @{{.Usernames}} no se li ha notificat aquesta menció perquè no està al canal. No es pot afegir al canal perquè no és membre dels grups enllaçats. Per afegir-lo a aquest canal, s’ha d’afegir als grups enllaçats." + }, + { + "id": "api.post.check_for_out_of_channel_groups_mentions.message.multiple", + "translation": "A @{{.Usernames}} i a @{{.LastUsername}} no se'ls ha notificat aquesta menció perquè no estan al canal. No es poden afegir al canal perquè no són membres dels grups enllaçats. Per afegir-los a aquest canal, s’han d’afegir als grups enllaçats." + }, + { + "id": "api.plugin.verify_plugin.app_error", + "translation": "No és possible verificar la signatura del connector." + }, + { + "id": "api.plugin.upload.no_file.app_error", + "translation": "Hi manca el fitxer en la sol·licitud multipart/form" + }, + { + "id": "api.plugin.upload.file.app_error", + "translation": "No s'ha pogut obrir el fitxer en la sol·licitud multipart/form" + }, + { + "id": "api.plugin.upload.array.app_error", + "translation": "La matriu de fitxers està buida en la sol·licitud multipart/form" + }, + { + "id": "api.plugin.install.download_failed.app_error", + "translation": "S'ha produït un error en descarregar el connector." + }, + { + "id": "api.outgoing_webhook.disabled.app_error", + "translation": "L'administrador del sistema ha desactivat els «webhooks» de sortida." + }, + { + "id": "api.oauth.singup_with_oauth.invalid_link.app_error", + "translation": "L'enllaç de registre no sembla que sigui vàlid" + }, + { + "id": "api.oauth.singup_with_oauth.expired_link.app_error", + "translation": "L'enllaç de registre ha caducat" + }, + { + "id": "api.oauth.singup_with_oauth.disabled.app_error", + "translation": "El registre d'usuaris està desactivat." + }, + { + "id": "api.oauth.revoke_access_token.get.app_error", + "translation": "S'ha produït un error en obtenir el testimoni d'accés des de la DB abans de l'eliminació" + }, + { + "id": "api.oauth.revoke_access_token.del_token.app_error", + "translation": "S'ha produït un error en suprimir el testimoni d'accés des de la DB" + }, + { + "id": "api.oauth.revoke_access_token.del_session.app_error", + "translation": "S'ha produït un error en suprimir la sessió des de la DB" + }, + { + "id": "api.oauth.register_oauth_app.turn_off.app_error", + "translation": "L'administrador del sistema ha desactivat el proveïdor de serveis OAuth2." + }, + { + "id": "api.oauth.invalid_state_token.app_error", + "translation": "Estat del testimoni no vàlid" + }, + { + "id": "api.oauth.get_access_token.refresh_token.app_error", + "translation": "invalid_grant: Testimoni d'actualització no vàlid" + }, + { + "id": "api.oauth.get_access_token.redirect_uri.app_error", + "translation": "invalid_request: El «redirect_uri» subministrat no coincideix amb el codi d'autorització «redirect_uri»" + }, + { + "id": "api.oauth.get_access_token.missing_refresh_token.app_error", + "translation": "invalid_request: Hi manca el «refresh_token»" + }, + { + "id": "api.oauth.get_access_token.missing_code.app_error", + "translation": "invalid_request: Hi manca el codi" + }, + { + "id": "api.oauth.get_access_token.internal_user.app_error", + "translation": "server_error: S'ha trobat un error intern del servidor mentre s'extreia l'usuari de la base de dades" + }, + { + "id": "api.oauth.get_access_token.internal_session.app_error", + "translation": "server_error: S'ha trobat un error intern del servidor mentre es desava la sessió a la base de dades" + }, + { + "id": "api.oauth.get_access_token.internal_saving.app_error", + "translation": "server_error: S'ha trobat un error intern del servidor mentre es desava el testimoni d'accés a la base de dades" + }, + { + "id": "api.oauth.get_access_token.internal.app_error", + "translation": "server_error: S'ha trobat un error del servidor intern en accedir a la base de dades" + }, + { + "id": "api.oauth.get_access_token.expired_code.app_error", + "translation": "invalid_grant: Codi d'autorització no vàlid o caducat" + }, + { + "id": "api.oauth.get_access_token.disabled.app_error", + "translation": "L'administrador del sistema ha desactivat el proveïdor de serveis OAuth2." + }, + { + "id": "api.oauth.get_access_token.credentials.app_error", + "translation": "invalid_client: Credencials de client no vàlides" + }, + { + "id": "api.oauth.get_access_token.bad_grant.app_error", + "translation": "invalid_request: «grant_type» incorrecte" + }, + { + "id": "api.oauth.get_access_token.bad_client_secret.app_error", + "translation": "invalid_request: Hi manca el «client_secret»" + }, + { + "id": "api.oauth.get_access_token.bad_client_id.app_error", + "translation": "invalid_request: «client_id» incorrecte" + }, + { + "id": "api.oauth.authorize_oauth.disabled.app_error", + "translation": "L'administrador del sistema ha desactivat el proveïdor de serveis OAuth2." + }, + { + "id": "api.oauth.allow_oauth.turn_off.app_error", + "translation": "L'administrador del sistema ha desactivat el proveïdor de serveis OAuth2." + }, + { + "id": "api.oauth.allow_oauth.redirect_callback.app_error", + "translation": "invalid_request: El «redirect_uri» subministrat no coincideix amb el «callback_url» registrat" + }, + { + "id": "api.marshal_error", + "translation": "marshal error" + }, + { + "id": "api.license.remove_expired_license.failed.error", + "translation": "S'ha produït un error en enviar el correu de desactivació del compte" + }, + { + "id": "api.license.client.old_format.app_error", + "translation": "El nou format de la llicència del client encara no és compatible. Especifica format=old a la cadena de consulta." + }, + { + "id": "api.license.add_license.unique_users.app_error", + "translation": "Aquesta llicència només admet {{.Users}} usuaris, quan el teu sistema té {{.Count}} usuaris únics. Els usuaris únics es recompten de forma clara per correu. Pots veure el recompte d'usuaris total a Informes del lloc -> Mostra les estadístiques." + }, + { + "id": "api.license.add_license.save_active.app_error", + "translation": "L'ID de llicència activa no s'ha desat correctament." + }, + { + "id": "api.license.add_license.save.app_error", + "translation": "La llicència no s'ha desat correctament." + }, + { + "id": "api.license.add_license.open.app_error", + "translation": "No s'ha pogut obrir el fitxer de llicència" + }, + { + "id": "api.license.add_license.no_file.app_error", + "translation": "Cap fitxer sota «llicència» a la petició" + }, + { + "id": "api.license.add_license.invalid_count.app_error", + "translation": "No s'ha pogut comptar el total d'usuaris únics." + }, + { + "id": "api.license.add_license.invalid.app_error", + "translation": "Fitxer de llicència no vàlid." + }, + { + "id": "api.license.add_license.expired.app_error", + "translation": "La llicència ja ha caducat o encara no ha començat." + }, + { + "id": "api.license.add_license.array.app_error", + "translation": "Matriu buida sota «llicència» a la petició" + }, + { + "id": "api.ldap_groups.license_error", + "translation": "La teva llicència no suporta: Idap groups" + }, + { + "id": "api.ldap_group.not_found", + "translation": "No s'ha trobat: Idap group" + }, + { + "id": "api.io_error", + "translation": "input/output error" + }, + { + "id": "api.incoming_webhook.invalid_username.app_error", + "translation": "Nom d'usuari no vàlid." + }, + { + "id": "api.incoming_webhook.disabled.app_error", + "translation": "L'administrador del sistema ha desactivat els «webhooks» entrants." + }, + { + "id": "api.file.upload_file.too_large_detailed.app_error", + "translation": "No es pot pujar el fitxer {{.Filename}}. {{.Length}} bytes excedeix el límit màxim permès de {{.Limit}} bytes." + }, + { + "id": "api.file.upload_file.storage.app_error", + "translation": "No es pot pujar el fitxer. L'emmagatzematge d'imatges no està configurat." + }, + { + "id": "api.file.upload_file.read_request.app_error", + "translation": "No es poden pujar els fitxers. S'ha produït un error en llegir o analitzar les dades de sol·licitud." + }, + { + "id": "api.file.upload_file.read_form_value.app_error", + "translation": "No es poden penjar els fitxers. S'ha produït un error en llegir el valor de {{.Formname}}." + }, + { + "id": "api.file.upload_file.multiple_channel_ids.app_error", + "translation": "No es poden penjar els fitxers. S'han produït múltiples conflictes amb els «channel_ids»." + }, + { + "id": "api.file.upload_file.large_image_detailed.app_error", + "translation": "Les dimensions de {{.Filename}} són ({{.Width}} per {{.Height}} píxels) i excedeixen els límits" + }, + { + "id": "api.file.upload_file.large_image.app_error", + "translation": "No es pot penjar un fitxer que supera les dimensions màximes: {{.Filename}}" + }, + { + "id": "api.file.upload_file.incorrect_number_of_client_ids.app_error", + "translation": "No es poden penjar els fitxers. Hi ha {{.NumClientIds}} «clients_ids» per a {{.NumFiles}} fitxers." + }, + { + "id": "api.file.read_file.reading_local.app_error", + "translation": "S'ha trobat un error en llegir des de l'emmagatzematge de fitxers del servidor local" + }, + { + "id": "api.file.no_driver.app_error", + "translation": "No s'ha seleccionat cap gestor de fitxers." + }, + { + "id": "api.file.get_public_link.no_post.app_error", + "translation": "No s'ha pogut obtenir un enllaç públic per al fitxer. El fitxer s'ha d'adjuntar a una publicació que l'usuari actual pugui llegir." + }, + { + "id": "api.file.get_public_link.disabled.app_error", + "translation": "S'han desactivat els enllaços públics" + }, + { + "id": "api.file.get_file_thumbnail.no_thumbnail.app_error", + "translation": "El fitxer no té una imatge en miniatura" + }, + { + "id": "api.file.get_file_preview.no_preview.app_error", + "translation": "El fitxer no té una imatge de previsualització" + }, + { + "id": "api.file.get_file.public_invalid.app_error", + "translation": "L'enllaç públic no sembla que sigui vàlid" + }, + { + "id": "api.file.attachments.disabled.app_error", + "translation": "Els fitxers adjunts s'han desactivat en aquest servidor." + }, + { + "id": "api.emoji.upload.open.app_error", + "translation": "No es pot crear l'emoji. S'ha produït un error en intentar obrir la imatge adjunta." + }, + { + "id": "api.emoji.upload.large_image.too_large.app_error", + "translation": "No s'ha pogut crear l'emoji. La imatge ha de ser més petita que {{.MaxWidth}} per {{.MaxHeight}}." + }, + { + "id": "api.emoji.upload.large_image.gif_encode_error", + "translation": "No s'ha pogut crear l'emoji. S'ha produït un error en intentar codificar la imatge GIF." + }, + { + "id": "api.emoji.upload.large_image.gif_decode_error", + "translation": "No s'ha pogut crear l'emoji. S'ha produït un error en intentar descodificar la imatge GIF." + }, + { + "id": "api.emoji.upload.large_image.encode_error", + "translation": "No s'ha pogut crear l'emoji. S'ha produït un error en intentar codificar la imatge." + }, + { + "id": "api.emoji.upload.large_image.decode_error", + "translation": "No s'ha pogut crear l'emoji. S'ha produït un error en intentar descodificar la imatge." + }, + { + "id": "api.emoji.upload.image.app_error", + "translation": "No s'ha pogut crear l'emoji. El tipus de fitxer ha de ser PNG, JPEG o GIF." + }, + { + "id": "api.emoji.storage.app_error", + "translation": "L'emmagatzematge de fitxers no s'ha configurat correctament. Configura'l per a l'emmagatzematge de fitxers del servidor local o S3." + }, + { + "id": "api.emoji.get_image.read.app_error", + "translation": "No s'ha pogut llegir el fitxer d'imatge per a l'emoji." + }, + { + "id": "api.emoji.get_image.decode.app_error", + "translation": "No s'ha pogut descodificar el fitxer d'imatge per a l'emoji." + }, + { + "id": "api.emoji.disabled.app_error", + "translation": "L'administrador del sistema ha desactivat els emojis personalitzats." + }, + { + "id": "api.emoji.create.too_large.app_error", + "translation": "No es pot crear l'emoji. La imatge ha de pesar menys d'1 MB." + }, + { + "id": "api.emoji.create.parse.app_error", + "translation": "No es pot crear l'emoji. No s'ha pogut entendre la sol·licitud." + }, + { + "id": "api.emoji.create.other_user.app_error", + "translation": "ID d'usuari no vàlid" + }, + { + "id": "api.emoji.create.duplicate.app_error", + "translation": "No es pot crear l'emoji. Ja existeix un altre amb el mateix nom." + }, + { + "id": "api.email_batching.send_batched_email_notification.subject", + "translation": { + "one": "Nova notificació de [{{.SiteName}}] a {{.Day}} {{.Month}}, {{.Year}}", + "other": "Noves notificacions de [{{.SiteName}}] a {{.Day}} {{.Month}}, {{.Year}}" + } + }, + { + "id": "api.email_batching.add_notification_email_to_batch.disabled.app_error", + "translation": "L'administrador del sistema ha desactivat el processament de correus per lots." + }, + { + "id": "api.email_batching.add_notification_email_to_batch.channel_full.app_error", + "translation": "El canal de recepció del treball per lots de correu estava ple. Augmenta l'EmailBatchingBufferSize." + }, + { + "id": "api.create_terms_of_service.empty_text.app_error", + "translation": "Introduïu el text per a les Condicions del servei personalitzat." + }, + { + "id": "api.create_terms_of_service.custom_terms_of_service_disabled.app_error", + "translation": "La funció de termes del servei personalitzat està desactivada" + }, + { + "id": "api.context.token_provided.app_error", + "translation": "La sessió no és del tipus OAuth, però s'ha proporcionat un testimoni a la cadena de consulta" + }, + { + "id": "api.context.session_expired.app_error", + "translation": "La sessió no és vàlida o ha caducat, torna a iniciar sessió." + }, + { + "id": "api.context.server_busy.app_error", + "translation": "El servidor està ocupat, els serveis no crítics no estan disponibles temporalment." + }, + { + "id": "api.context.permissions.app_error", + "translation": "No teniu els permisos apropiats" + }, + { + "id": "api.context.mfa_required.app_error", + "translation": "Cal autenticació multifactor en aquest servidor." + }, + { + "id": "api.context.invalid_url_param.app_error", + "translation": "El paràmetre {{.Name}} no és vàlid o no es troba a l'URL sol·licitada" + }, + { + "id": "api.context.invalid_token.error", + "translation": "El testimoni {{.Token}} de sessió no és vàlid, err={{.Error}}" + }, + { + "id": "api.context.invalid_param.app_error", + "translation": "El paràmetre {{.Name}} no és vàlid" + }, + { + "id": "api.context.invalid_body_param.app_error", + "translation": "{{.Nom}} no és vàlid o manca en el cos de la sol·licitud" + }, + { + "id": "api.context.404.app_error", + "translation": "Ens sap greu, no hem pogut trobar la pàgina." + }, + { + "id": "api.config.update_config.restricted_merge.app_error", + "translation": "No s'ha pogut combinar la configuració proporcionada." + }, + { + "id": "api.config.client.old_format.app_error", + "translation": "El nou format per a la configuració del client encara no és compatible. Especifica format=old a la cadena de consulta." + }, + { + "id": "api.command_shrug.name", + "translation": "shrug" + }, + { + "id": "api.command_shrug.hint", + "translation": "[missatge]" + }, + { + "id": "api.command_shrug.desc", + "translation": "Afegeix ¯\\_(ツ)_/¯ al teu missatge" + }, + { + "id": "api.command_shortcuts.unsupported.app_error", + "translation": "L'ordre de les dreceres no és compatible amb el dispositiu" + }, + { + "id": "api.command_shortcuts.name", + "translation": "shortcuts" + }, + { + "id": "api.command_shortcuts.desc", + "translation": "Mostra una llista de dreceres de teclat" + }, + { + "id": "api.command_settings.unsupported.app_error", + "translation": "L'ordre de configuració no és compatible amb el dispositiu" + }, + { + "id": "api.command_settings.name", + "translation": "settings" + }, + { + "id": "api.command_settings.desc", + "translation": "Obre el diàleg de configuració del compte" + }, + { + "id": "api.command_search.unsupported.app_error", + "translation": "L'ordre de cerca no és compatible amb el dispositiu" + }, + { + "id": "api.command_search.name", + "translation": "search" + }, + { + "id": "api.command_search.hint", + "translation": "[text]" + }, + { + "id": "api.command_search.desc", + "translation": "Cerca text als missatges" + }, + { + "id": "api.command_remove.user_not_in_channel", + "translation": "{{.Username}} no és membre d'aquest canal." + }, + { + "id": "api.command_remove.permission.app_error", + "translation": "No tens els permisos adequats per eliminar el membre." + }, + { + "id": "api.command_remove.name", + "translation": "remove" + }, + { + "id": "api.command_remove.missing.app_error", + "translation": "No hem pogut trobar l'usuari. Pot ser que l'administrador del sistema l'hagi desactivat." + }, + { + "id": "app.import.process_import_data_file_version_line.invalid_version.error", + "translation": "No es pot llegir la versió del fitxer d'importació de dades." + }, + { + "id": "app.import.import_user_teams.save_preferences.error", + "translation": "No s'han pogut desar les preferències del tema de l'equip" + }, + { + "id": "app.import.import_user_channels.save_preferences.error", + "translation": "S'ha produït un error en importar les subscripcions del canal. No s'ha pogut desar les preferències." + }, + { + "id": "app.import.import_user.save_preferences.error", + "translation": "S'ha produït un error en importar les preferències de l'usuari. No s'ha pogut desar les preferències." + }, + { + "id": "app.import.import_team.scheme_wrong_scope.error", + "translation": "L'equip s'ha d'assignar a un esquema de l'àmbit del canal." + }, + { + "id": "app.import.import_team.scheme_deleted.error", + "translation": "No es pot establir un esquema eliminat a un equip." + }, + { + "id": "app.import.import_scheme.scope_change.error", + "translation": "L'importador massiu no pot canviar l'abast d'un esquema ja existent." + }, + { + "id": "app.import.import_post.user_not_found.error", + "translation": "S'ha produït un error en importar la publicació. No s'ha pogut trobar l'usuari \"{{.Username}}\"." + }, + { + "id": "app.import.import_post.save_preferences.error", + "translation": "S'ha produït un error en importar la publicació. No s'ha pogut desar les preferències." + }, + { + "id": "app.import.import_post.channel_not_found.error", + "translation": "S'ha produït un error en importar la publicació. No s'ha pogut trobar el canal amb el nom \"{{.ChannelName}}\"." + }, + { + "id": "app.import.import_line.unknown_line_type.error", + "translation": "La línia d'importació de dades té un tipus desconegut \"{{.Type}}\"." + }, + { + "id": "app.import.import_line.null_user.error", + "translation": "La línia d'importació de dades té el tipus «user» però l'objecte d'usuari és nul." + }, + { + "id": "app.import.import_line.null_team.error", + "translation": "La línia d'importació de dades té el tipus «team» però l'objecte de l'equip és nul." + }, + { + "id": "app.import.import_line.null_scheme.error", + "translation": "La línia d'importació de dades té el tipus «scheme» però l'objecte d'esquema és nul." + }, + { + "id": "app.import.import_line.null_post.error", + "translation": "La línia d'importació de dades té el tipus «post» però l'objecte de publicació és nul." + }, + { + "id": "app.import.import_line.null_emoji.error", + "translation": "La línia d'importació de dades té el tipus «emoji» però l'objecte de l'emoji és nul." + }, + { + "id": "app.import.import_line.null_direct_post.error", + "translation": "La línia d'importació de dades té el tipus «canal» però l'objecte de publicació directa és nul." + }, + { + "id": "app.import.import_line.null_direct_channel.error", + "translation": "La línia d'importació de dades té el tipus «direct_channel» però l'objecte del canal directe és nul." + }, + { + "id": "app.import.import_line.null_channel.error", + "translation": "La línia d'importació de dades té el tipus «channel» però l'objecte del canal és nul." + }, + { + "id": "app.import.import_direct_post.create_group_channel.error", + "translation": "No s'ha pogut obtenir el canal grupal" + }, + { + "id": "app.import.import_direct_post.create_direct_channel.error", + "translation": "No s'ha pogut obtenir el canal directe" + }, + { + "id": "app.import.import_direct_channel.update_header_failed.error", + "translation": "No s'ha pogut actualitzar l'encapçalament del canal directe" + }, + { + "id": "app.import.import_direct_channel.create_group_channel.error", + "translation": "No s'ha pogut crear el canal grupal" + }, + { + "id": "app.import.import_direct_channel.create_direct_channel.error", + "translation": "No s'ha pogut crear el canal directe" + }, + { + "id": "app.import.import_channel.team_not_found.error", + "translation": "S'ha produït un error en importar el canal. No s'ha pogut trobar l'equip amb el nom \"{{.TeamName}}\"." + }, + { + "id": "app.import.import_channel.scheme_wrong_scope.error", + "translation": "El canal s'ha d'assignar a un esquema de l'àmbit del canal." + }, + { + "id": "app.import.import_channel.scheme_deleted.error", + "translation": "No es pot establir un esquema eliminat a un canal." + }, + { + "id": "app.import.emoji.bad_file.error", + "translation": "S'ha produït un error en llegir el fitxer importat amb la imatge de l'emoji. Nom de l'emoji: \"{{.EmojiName}}\"" + }, + { + "id": "app.import.bulk_import.unsupported_version.error", + "translation": "Versió incorrecta o hi manca al fitxer d'importació de dades. Assegura't que la versió sigui el primer objecte al fitxer d'importació i torna-ho a provar." + }, + { + "id": "app.import.bulk_import.json_decode.error", + "translation": "S'ha produït un error en decodificar la línia JSON." + }, + { + "id": "app.import.bulk_import.file_scan.error", + "translation": "Error en llegir el fitxer d'importació de dades." + }, + { + "id": "app.import.attachment.file_upload.error", + "translation": "Error en pujar el fitxer: \"{{.FilePath}}\"" + }, + { + "id": "app.import.attachment.bad_file.error", + "translation": "Error en llegir el fitxer a: \"{{.FilePath}}\"" + }, + { + "id": "app.export.export_write_line.json_marshall.error", + "translation": "S'ha produït un error en reunir les dades JSON per exportar-les." + }, + { + "id": "app.export.export_write_line.io_writer.error", + "translation": "S'ha produït un error en escriure les dades d'exportació." + }, + { + "id": "app.export.export_custom_emoji.copy_emoji_images.error", + "translation": "No s'han pogut copiar les imatges d'emoticones personalitzats" + }, + { + "id": "app.channel.post_update_channel_purpose_message.updated_to", + "translation": "%s ha actualitzat el propòsit del canal a: %s" + }, + { + "id": "app.channel.post_update_channel_purpose_message.updated_from", + "translation": "%s ha actualitzat el propòsit del canal de: %s a: %s" + }, + { + "id": "app.channel.post_update_channel_purpose_message.retrieve_user.error", + "translation": "No s'ha pogut recuperar l'usuari durant l'actualització de la capçalera del canal %v" + }, + { + "id": "app.channel.post_update_channel_purpose_message.removed", + "translation": "%s ha eliminat el propòsit del canal (era: %s)" + }, + { + "id": "app.channel.post_update_channel_purpose_message.post.error", + "translation": "No s'ha pogut publicar el missatge del propòsit del canal" + }, + { + "id": "app.channel.move_channel.members_do_not_match.error", + "translation": "No es pot moure un canal, tret que tots els seus membres ja siguin membres de l'equip de destinació." + }, + { + "id": "app.channel.create_direct_channel.internal_error", + "translation": "No es pot desar el canal" + }, + { + "id": "app.channel.create_channel.no_team_id.app_error", + "translation": "Has d'especificar l'ID de l'equip per crear un canal" + }, + { + "id": "app.channel.create_channel.internal_error", + "translation": "No es pot desar el canal" + }, + { + "id": "app.bot.permenent_delete.bad_id", + "translation": "No es pot eliminar el bot" + }, + { + "id": "app.bot.patchbot.internal_error", + "translation": "No es pot actualitzar el bot" + }, + { + "id": "app.bot.getbots.internal_error", + "translation": "No es poden obtenir els bots" + }, + { + "id": "app.bot.getbot.internal_error", + "translation": "No es pot obtenir el bot" + }, + { + "id": "app.bot.createbot.internal_error", + "translation": "No es pot desar el bot" + }, + { + "id": "app.admin.test_email.failure", + "translation": "S'ha produït un error en la connexió: {{.Error}}" + }, + { + "id": "api.websocket_handler.server_busy.app_error", + "translation": "El servidor està ocupat, els serveis no crítics no estan disponibles temporalment." + }, + { + "id": "api.websocket_handler.invalid_param.app_error", + "translation": "El paràmetre {{.Name}} no és vàlid" + }, + { + "id": "api.webhook.update_outgoing.intersect.app_error", + "translation": "Els «webhooks» sortints del mateix canal no poden tenir les mateixes paraules d'activació/URL de retorn." + }, + { + "id": "api.webhook.team_mismatch.app_error", + "translation": "No es pot actualitzar el «webhook» a través d'equips" + }, + { + "id": "api.webhook.incoming.error", + "translation": "No s'ha pogut codificar la càrrega multipart al «webhook» entrant." + }, + { + "id": "api.webhook.create_outgoing.triggers.app_error", + "translation": "Cal configurar les «trigger_words» o el «channel_id»" + }, + { + "id": "api.webhook.create_outgoing.permissions.app_error", + "translation": "Permisos no vàlids per crear un «webhook» sortint." + }, + { + "id": "api.webhook.create_outgoing.not_open.app_error", + "translation": "Els «webhooks» sortints només es poden crear per a canals públics." + }, + { + "id": "api.webhook.create_outgoing.intersect.app_error", + "translation": "Els «webhooks» sortints del mateix canal no poden tenir les mateixes paraules d'activació/URL de retorn." + }, + { + "id": "api.web_socket_router.not_authenticated.app_error", + "translation": "La connexió del WebSocket no està autenticada. Inicia sessió i torna-ho a provar." + }, + { + "id": "api.web_socket_router.no_action.app_error", + "translation": "Cap acció del websocket." + }, + { + "id": "api.web_socket_router.bad_seq.app_error", + "translation": "Seqüència no vàlida per al missatge del WebSocket." + }, + { + "id": "api.web_socket_router.bad_action.app_error", + "translation": "Acció del WebSocket desconeguda." + }, + { + "id": "api.web_socket.connect.upgrade.app_error", + "translation": "S'ha produït un error en actualitzar la connexió del websocket" + }, + { + "id": "api.user.verify_email.token_parse.error", + "translation": "No s'ha pogut analitzar les dades del testimoni des de la verificació del correu" + }, + { + "id": "api.user.verify_email.link_expired.app_error", + "translation": "L'enllaç de verificació del correu ha caducat." + }, + { + "id": "api.user.verify_email.broken_token.app_error", + "translation": "Tipus de testimoni de verificació de correu incorrecte." + }, + { + "id": "api.user.verify_email.bad_link.app_error", + "translation": "Enllaç de verificació de correu incorrecte." + }, + { + "id": "api.user.upload_profile_user.upload_profile.app_error", + "translation": "No s'ha pogut pujar la imatge del perfil" + }, + { + "id": "api.user.upload_profile_user.too_large.app_error", + "translation": "No s'ha pogut pujar la imatge de perfil. El fitxer és massa gran." + }, + { + "id": "api.user.upload_profile_user.storage.app_error", + "translation": "No es pot pujar el fitxer. L'emmagatzematge d'imatges no està configurat." + }, + { + "id": "api.user.upload_profile_user.parse.app_error", + "translation": "No s'ha pogut parsejar el formulari multipart" + }, + { + "id": "api.user.upload_profile_user.open.app_error", + "translation": "No s'ha pogut obrir el fitxer d'imatge" + }, + { + "id": "api.user.upload_profile_user.no_file.app_error", + "translation": "Cap fitxer sota «imatge» a la petició" + }, + { + "id": "api.user.upload_profile_user.encode.app_error", + "translation": "No s'ha pogut codificar la imatge del perfil" + }, + { + "id": "api.user.upload_profile_user.decode.app_error", + "translation": "No s'ha pogut decodificar la imatge del perfil" + }, + { + "id": "api.user.upload_profile_user.array.app_error", + "translation": "Matriu buida sota 'image' a la petició" + }, + { + "id": "api.user.update_user.accepted_guest_domain.app_error", + "translation": "El correu electrònic que has proporcionat no pertany a un domini permès. Contacta amb el teu administrador o registra't amb un correu diferent." + }, + { + "id": "api.user.update_user.accepted_domain.app_error", + "translation": "El correu electrònic que has proporcionat no pertany a un domini permès. Contacta amb el teu administrador o registra't amb un correu diferent." + }, + { + "id": "api.user.update_password.valid_account.app_error", + "translation": "L'actualització de la contrasenya ha fallat perquè no hem pogut trobar un compte vàlid" + }, + { + "id": "api.user.update_password.oauth.app_error", + "translation": "L'actualització de la contrasenya ha fallat a causa de que l'usuari està vinculat a un proveïdor d'OAuth" + }, + { + "id": "api.user.update_password.menu", + "translation": "utilitzant el menú de configuració" + }, + { + "id": "api.user.update_password.incorrect.app_error", + "translation": "La «contrasenya actual» que has introduït no és correcta. Comprova que les majúscules estiguin desactivades i torna-ho a provar." + }, + { + "id": "api.user.update_password.failed.app_error", + "translation": "S'ha produït un error en actualitzar la contrasenya" + }, + { + "id": "api.user.update_password.context.app_error", + "translation": "S'ha produït un error en actualitzar la contrasenya perquè l'user_id de context no coincideix amb l'user_id de props" + }, + { + "id": "api.user.update_oauth_user_attrs.get_user.app_error", + "translation": "No s'ha pogut obtenir l'usuari des de l'objecte de {{.Service}}" + }, + { + "id": "api.user.update_active.permissions.app_error", + "translation": "No teniu els permisos apropiats" + }, + { + "id": "api.user.update_active.not_enable.app_error", + "translation": "No pots desactivar-te a tú mateix perquè aquesta funció no està activada. Contacta amb el teu administrador del sistema." + }, + { + "id": "api.command_remove.message.app_error", + "translation": "S'ha de proporcionar un missatge amb l'ordre /remove o /kick." + }, + { + "id": "api.command_remove.hint", + "translation": "@[usuari]" + }, + { + "id": "api.command_remove.group_constrained_user_denied", + "translation": "No pots expulsar l'usuari del canal perquè és membre d'altres grups enllaçats a aquest canal. Per expulsar-lo d'aquest canal, ha de ser expulsat dels grups enllaçats." + }, + { + "id": "api.command_remove.direct_group.app_error", + "translation": "No pots eliminar a algú d'un canal de missatge directe." + }, + { + "id": "api.command_remove.desc", + "translation": "Suprimeix un membre del canal" + }, + { + "id": "api.command_open.name", + "translation": "open" + }, + { + "id": "api.command_online.success", + "translation": "Ara esteu connectat" + }, + { + "id": "api.command_online.name", + "translation": "online" + }, + { + "id": "api.command_online.desc", + "translation": "Estableix l'estat a en línia" + }, + { + "id": "api.command_offline.success", + "translation": "Ara esteu desconnectat" + }, + { + "id": "api.command_offline.name", + "translation": "offline" + }, + { + "id": "api.command_offline.desc", + "translation": "Estableix l'estat a fora de línia" + }, + { + "id": "api.command_mute.success_unmute_direct_msg", + "translation": "Aquest canal ja no està silenciat." + }, + { + "id": "api.command_mute.success_unmute", + "translation": "{{.Channel}} ja no està silenciat." + }, + { + "id": "api.command_mute.success_mute_direct_msg", + "translation": "No rebràs notificacions d'aquest canal mentrestant el canal estigui silenciat." + }, + { + "id": "api.command_mute.success_mute", + "translation": "No rebràs notificacions per {{.Channel}} mentrestant el canal estigui silenciat." + }, + { + "id": "api.command_mute.not_member.error", + "translation": "No s'ha pogut silenciar el canal {{.Channel}} perquè no ets membre." + }, + { + "id": "api.command_mute.no_channel.error", + "translation": "No s'ha pogut trobar el canal especificat. Utilitza l'[identificador del canal](https://about.mattermost.com/default-channel-handle-documentation) per identificar canals." + }, + { + "id": "api.command_mute.name", + "translation": "mute" + }, + { + "id": "api.command_mute.hint", + "translation": "~[canal]" + }, + { + "id": "api.command_mute.error", + "translation": "No s'ha pogut trobar el canal {{.Channel}}. Utilitza l'[identificador del canal](https://about.mattermost.com/default-channel-handle-documentation) per identificar canals." + }, + { + "id": "api.command_mute.desc", + "translation": "Desactiva les notificacions d'escriptori, de correu electrònic i de tramesa automàtica per al canal actual o el [canal] especificat." + }, + { + "id": "api.command_msg.permission.app_error", + "translation": "No tens els permisos adequats per enviar missatges directes a aquest usuari." + }, + { + "id": "api.command_msg.name", + "translation": "message" + }, + { + "id": "api.command_msg.missing.app_error", + "translation": "No es pot trobar l'usuari." + }, + { + "id": "api.command_msg.hint", + "translation": "@[usuari] «missatge»" + }, + { + "id": "api.command_msg.fail.app_error", + "translation": "S'ha produït un error en enviar missatges a l'usuari." + }, + { + "id": "api.command_msg.dm_fail.app_error", + "translation": "S'ha produït un error en crear el missatge directe." + }, + { + "id": "api.command_msg.desc", + "translation": "Envia un missatge directe a un usuari" + }, + { + "id": "api.command_me.name", + "translation": "me" + }, + { + "id": "api.command_me.hint", + "translation": "[missatge]" + }, + { + "id": "api.command_me.desc", + "translation": "Fes una acció" + }, + { + "id": "api.command_logout.name", + "translation": "logout" + }, + { + "id": "api.command_logout.desc", + "translation": "Tanca sessió de Mattermost" + }, + { + "id": "api.command_leave.name", + "translation": "leave" + }, + { + "id": "api.command_leave.fail.app_error", + "translation": "S'ha produït un error en deixar del canal." + }, + { + "id": "api.command_leave.desc", + "translation": "Deixa el canal actual" + }, + { + "id": "api.command_kick.name", + "translation": "kick" + }, + { + "id": "api.command_join.name", + "translation": "join" + }, + { + "id": "api.command_join.missing.app_error", + "translation": "No es pot trobar el canal" + }, + { + "id": "api.command_join.list.app_error", + "translation": "S'ha produït un error en mostrar els canals." + }, + { + "id": "api.command_join.hint", + "translation": "~[canal]" + }, + { + "id": "api.command_join.fail.app_error", + "translation": "S'ha produït un error en unir-se al canal." + }, + { + "id": "api.command_join.desc", + "translation": "Uneix-te al canal obert" + }, + { + "id": "api.command_invite_people.permission.app_error", + "translation": "No tens permís per convidar usuaris nous a aquest servidor." + }, + { + "id": "api.command_invite.user_not_in_team.app_error", + "translation": "@{{.Username}} no és membre d'aquest canal." + }, + { + "id": "api.command_invite.user_already_in_channel.app_error", + "translation": "{{.User}} ja està al canal." + }, + { + "id": "api.command_invite.success", + "translation": "{{.User}} s'ha afegit al canal {{.Channel}}." + }, + { + "id": "api.command_invite.private_channel.app_error", + "translation": "No s'ha pogut trobar el canal {{.Channel}}. Utilitza l'identificador del canal per identificar-los." + }, + { + "id": "api.command_invite.permission.app_error", + "translation": "No tens prou permisos per afegir {{.User}} a {{.Channel}}." + }, + { + "id": "api.command_invite.name", + "translation": "invite" + }, + { + "id": "api.command_invite.missing_user.app_error", + "translation": "No hem pogut trobar l'usuari. És possible que l'administrador del sistema l'hagi desactivat." + }, + { + "id": "api.command_invite.missing_message.app_error", + "translation": "Falta el nom d'usuari i el canal." + }, + { + "id": "api.command_invite.hint", + "translation": "@[usuari] ~[canal]" + }, + { + "id": "api.command_invite.group_constrained_user_denied", + "translation": "Aquest canal està gestionat per grups, però aquest usuari no pertany a cap d'ells." + }, + { + "id": "api.command_invite.fail.app_error", + "translation": "S'ha produït un error en unir-se al canal." + }, + { + "id": "api.command_invite.directchannel.app_error", + "translation": "No pots afegir algú a un canal de missatge directe." + }, + { + "id": "api.command_invite.desc", + "translation": "Convida un usuari a un canal" + }, + { + "id": "api.command_invite.channel.error", + "translation": "No s'ha pogut trobar el canal {{.Channel}}. Utilitza l'[identificador del canal](https://about.mattermost.com/default-channel-handle-documentation) per identificar canals." + }, + { + "id": "api.command_invite.channel.app_error", + "translation": "S'ha produït un error en recuperar el canal actual." + }, + { + "id": "api.command_help.name", + "translation": "help" + }, + { + "id": "api.command_help.desc", + "translation": "Obre la pàgina d'ajuda de Mattermost" + }, + { + "id": "api.command_groupmsg.permission.app_error", + "translation": "No tens els permisos adequats per crear un missatge de grup nou." + }, + { + "id": "api.command_groupmsg.name", + "translation": "missatge" + }, + { + "id": "api.command_groupmsg.min_users.app_error", + "translation": "Els missatges grupals estan limitats a un mínim de {{.MinUsers}} usuaris." + }, + { + "id": "api.command_groupmsg.max_users.app_error", + "translation": "Els missatges grupals estan limitats a un màxim de {{.MaxUsers}} usuaris." + }, + { + "id": "api.command_groupmsg.invalid_user.app_error", + "translation": { + "one": "No es pot trobar l'usuari: {{.Users}}", + "other": "No es poden trobar els usuaris: {{.Users}}" + } + }, + { + "id": "api.command_groupmsg.hint", + "translation": "@[usuari1],@[usuari2] «missatge»" + }, + { + "id": "api.command_groupmsg.group_fail.app_error", + "translation": "S'ha produït un error en crear el missatge grupal." + }, + { + "id": "api.command_groupmsg.fail.app_error", + "translation": "S'ha produït un error en enviar missatges als usuaris." + }, + { + "id": "api.command_groupmsg.desc", + "translation": "Envia un missatge grupal als usuaris especificats" + }, + { + "id": "api.command_expand_collapse.fail.app_error", + "translation": "S'ha produït un error en ampliar les previsualitzacions" + }, + { + "id": "api.command_expand.success", + "translation": "Els enllaços d'imatges s'amplien per defecte" + }, + { + "id": "api.command_expand.name", + "translation": "expand" + }, + { + "id": "api.command_expand.desc", + "translation": "Desactiva el replegament automàtic de les previsualitzacions d'imatges" + }, + { + "id": "api.command_echo.name", + "translation": "echo" + }, + { + "id": "api.command_echo.message.app_error", + "translation": "S'ha de proporcionar un missatge amb l'ordre /echo." + }, + { + "id": "api.command_echo.hint", + "translation": "«missatge» [retard en segons]" + }, + { + "id": "api.command_echo.high_volume.app_error", + "translation": "Massa sol·licituds «echo», no es pot processar la sol·licitud" + }, + { + "id": "api.command_echo.desc", + "translation": "Retireu el text del vostre compte." + }, + { + "id": "api.command_echo.delay.app_error", + "translation": "El retard ha de ser inferior a 10.000 segons" + }, + { + "id": "api.command_dnd.success", + "translation": "No molestar està activat. No rebràs notificacions de tramesa automàtica d'escriptori o mòbil fins que no desactivis No molestar." + }, + { + "id": "api.command_dnd.name", + "translation": "dnd" + }, + { + "id": "api.command_dnd.desc", + "translation": "No molestar desactiva les notificacions de tramesa automàtica d'escriptoris i mòbils." + }, + { + "id": "api.command_collapse.success", + "translation": "Els enllaços de les imatges es replegaran per defecte" + }, + { + "id": "api.command_collapse.name", + "translation": "collapse" + }, + { + "id": "api.command_collapse.desc", + "translation": "Activa el replegament automàtic de les previsualitzacions d'imatges" + }, + { + "id": "api.command_code.name", + "translation": "code" + }, + { + "id": "api.command_code.message.app_error", + "translation": "S'ha de proporcionar un missatge amb l'ordre /code." + }, + { + "id": "api.command_code.hint", + "translation": "[text]" + }, + { + "id": "api.command_code.desc", + "translation": "Mostra el text com a bloc de codi" + }, + { + "id": "api.command_channel_rename.update_channel.app_error", + "translation": "S'ha produït un error en actualitzar el canal actual." + }, + { + "id": "api.command_channel_rename.too_short.app_error", + "translation": "El nom del canal ha de tenir {{.Length}} o més caràcters" + }, + { + "id": "api.command_channel_rename.too_long.app_error", + "translation": "El nom del canal ha de tenir {{.Length}} o menys caràcters" + }, + { + "id": "api.command_channel_rename.permission.app_error", + "translation": "No tens els permisos adequats per canviar el nom del canal." + }, + { + "id": "api.command_channel_rename.name", + "translation": "rename" + }, + { + "id": "api.command_channel_rename.message.app_error", + "translation": "S'ha de proporcionar un missatge amb la comanda /rename." + }, + { + "id": "api.command_channel_rename.hint", + "translation": "[text]" + }, + { + "id": "api.command_channel_rename.direct_group.app_error", + "translation": "No es pot canviar el nom dels canals de missatges directes." + }, + { + "id": "api.command_channel_rename.desc", + "translation": "Canvia el nom del canal" + }, + { + "id": "api.command_channel_rename.channel.app_error", + "translation": "S'ha produït un error en recuperar el canal actual." + }, + { + "id": "api.command_channel_remove.channel.app_error", + "translation": "S'ha produït un error en recuperar el canal actual." + }, + { + "id": "api.command_channel_purpose.update_channel.app_error", + "translation": "S'ha produït un error en actualitzar el canal actual." + }, + { + "id": "api.command_channel_purpose.permission.app_error", + "translation": "No tens els permisos adequats per editar el propòsit del canal." + }, + { + "id": "api.command_channel_purpose.name", + "translation": "purpose" + }, + { + "id": "api.command_channel_purpose.message.app_error", + "translation": "S'ha de proporcionar un missatge amb l'ordre /purpose." + }, + { + "id": "api.command_channel_purpose.hint", + "translation": "[text]" + }, + { + "id": "api.command_channel_purpose.direct_group.app_error", + "translation": "No es pot establir el propòsit dels canals de missatges directes. Utilitza /header per establir la capçalera al seu lloc." + }, + { + "id": "api.command_channel_purpose.desc", + "translation": "Edita el propòsit del canal" + }, + { + "id": "api.command_channel_purpose.channel.app_error", + "translation": "S'ha produït un error en recuperar el canal actual." + }, + { + "id": "api.command_channel_header.update_channel.app_error", + "translation": "S'ha produït un error en actualitzar el canal actual." + }, + { + "id": "api.command_channel_header.permission.app_error", + "translation": "No tens els permisos adequats per editar la capçalera del canal." + }, + { + "id": "api.command_channel_header.name", + "translation": "header" + }, + { + "id": "api.command_channel_header.message.app_error", + "translation": "S'ha de proporcionar el text amb la comanda /header." + }, + { + "id": "api.command_channel_header.hint", + "translation": "[text]" + }, + { + "id": "api.command_channel_header.desc", + "translation": "Edita la capçalera del canal" + }, + { + "id": "api.command_channel_header.channel.app_error", + "translation": "S'ha produït un error en recuperar el canal actual." + }, + { + "id": "api.command_away.success", + "translation": "Ara esteu absent" + }, + { + "id": "api.command_away.name", + "translation": "away" + }, + { + "id": "api.command_away.desc", + "translation": "Establiu el vostre estat com a absent" + }, + { + "id": "api.command.team_mismatch.app_error", + "translation": "No es poden actualitzar les ordres a través dels equips" + }, + { + "id": "api.command.invite_people.sent", + "translation": "S'han enviat els correus d'invitació" + }, + { + "id": "api.command.invite_people.no_email", + "translation": "Especifica una o més adreces de correu vàlides" + }, + { + "id": "api.command.invite_people.name", + "translation": "invite_people" + }, + { + "id": "api.command.invite_people.invite_off", + "translation": "La creació d'usuaris s'ha desactivat en aquest servidor, no s'ha enviat cap invitació" + }, + { + "id": "api.command.invite_people.hint", + "translation": "[nom@domini.cat ...]" + }, + { + "id": "api.command.invite_people.fail", + "translation": "S'ha trobat un error en enviar els correus d'invitació" + }, + { + "id": "api.command.invite_people.email_off", + "translation": "El servei de correu no s'ha configurat, no s'ha enviat cap invitació" + }, + { + "id": "api.command.invite_people.email_invitations_off", + "translation": "Les invitacions per correu estan desactivades, no s'ha enviat cap invitació" + }, + { + "id": "api.command.invite_people.desc", + "translation": "Envia un correu d'invitació al teu equip de Mattermost" + }, + { + "id": "api.command.execute_command.start.app_error", + "translation": "No s'ha trobat cap disparador d'ordres" + }, + { + "id": "api.command.execute_command.not_found.app_error", + "translation": "No s'ha trobat una ordre amb un disparador de «{{.Trigger}}». Per enviar un missatge que comença per «/», prova a afegir un espai buit al principi del missatge." + }, + { + "id": "api.command.execute_command.failed_resp.app_error", + "translation": "L'ordre amb un disparador de «{{.Trigger}}» ha retornat la resposta {{.Status}}" + }, + { + "id": "api.command.execute_command.failed_empty.app_error", + "translation": "L'ordre amb un disparador de «{{.Trigger}}» ha retornat una resposta buida" + }, + { + "id": "api.command.execute_command.failed.app_error", + "translation": "S'ha produït un error amb un disparador de «{{.Trigger}}»" + }, + { + "id": "api.command.execute_command.create_post_failed.app_error", + "translation": "L'ordre '{{.Trigger}}' no ha pogut publicar la resposta. Contacta amb el teu administrador del sistema." + }, + { + "id": "api.command.duplicate_trigger.app_error", + "translation": "Aquesta paraula disparadora ja està en ús. Escull una altra." + }, + { + "id": "api.command.disabled.app_error", + "translation": "L'administrador del sistema ha desactivat les ordres." + }, + { + "id": "api.command.command_post.forbidden.app_error", + "translation": "L'usuari especificat no és membre del canal especificat." + }, + { + "id": "api.command.admin_only.app_error", + "translation": "Les integracions només s'han limitat als administradors." + }, + { + "id": "api.channel.update_team_member_roles.scheme_role.app_error", + "translation": "El rol proporcionat és gestionat per un esquema i, per tant, no es pot aplicar directament a un membre de l'equip" + }, + { + "id": "api.channel.update_team_member_roles.changing_guest_role.app_error", + "translation": "Actualització del membre de l'equip invàlida: no pots afegir o eliminar manualment el rol de convidat." + }, + { + "id": "api.channel.update_channel_scheme.scheme_scope.error", + "translation": "No es pot establir l'esquema al canal perquè l'esquema subministrat no és un esquema de canal." + }, + { + "id": "api.channel.update_channel_scheme.license.error", + "translation": "La teva llicència no és compatible amb l'actualització de l'esquema d'un canal" + }, + { + "id": "api.channel.update_channel_privacy.default_channel_error", + "translation": "El canal per defecte no es pot passar a privat." + }, + { + "id": "api.channel.update_channel_member_roles.scheme_role.app_error", + "translation": "El rol proporcionat és gestionat per un esquema i, per tant, no es pot aplicar directament a un membre del canal" + }, + { + "id": "api.channel.update_channel_member_roles.guest_and_user.app_error", + "translation": "Actualització del membre del canal invàlida: un usuari ha de ser convidat o usuari, però no els dos a la vegada." + }, + { + "id": "api.channel.update_channel_member_roles.changing_guest_role.app_error", + "translation": "Actualització del membre del canal invàlida: no pots afegir o eliminar manualment el rol de convidat." + }, + { + "id": "api.channel.update_channel.typechange.app_error", + "translation": "Aquest tipus de canal no es pot actualitzar" + }, + { + "id": "api.channel.update_channel.tried.app_error", + "translation": "S'ha intentat realitzar una actualització no vàlida del canal per defecte {{.Channel}}" + }, + { + "id": "api.channel.update_channel.deleted.app_error", + "translation": "El canal ha estat arxivat o suprimit" + }, + { + "id": "api.channel.restore_channel.unarchived", + "translation": "{{.Username}} ha arxivat el canal." + }, + { + "id": "api.channel.restore_channel.restored.app_error", + "translation": "No es pot desarxivar el canal perquè no està arxivat." + }, + { + "id": "api.channel.rename_channel.cant_rename_group_messages.app_error", + "translation": "No pots canviar el nom d'un canal de missatges grupals" + }, + { + "id": "api.channel.rename_channel.cant_rename_direct_messages.app_error", + "translation": "No pots canviar el nom d'un canal de missatges directes" + }, + { + "id": "api.channel.remove_user_from_channel.app_error", + "translation": "No es pot eliminar l’usuari d’aquest tipus de canal" + }, + { + "id": "api.channel.remove_members.denied", + "translation": "S'ha denegat l'expulsió dels canals als usuaris següents a causa de restriccions de grup:" + }, + { + "id": "api.channel.remove_member.removed", + "translation": "%v eliminat del canal." + }, + { + "id": "api.channel.remove_member.group_constrained.app_error", + "translation": "No es pot expulsar un usuari d'un canal restringit." + }, + { + "id": "api.channel.remove_channel_member.type.app_error", + "translation": "No es pot eliminar l'usuari d'un canal." + }, + { + "id": "api.channel.remove.default.app_error", + "translation": "No es pot eliminar l'usuari del canal per defecte {{.Channel}}" + }, + { + "id": "api.channel.post_user_add_remove_message_and_forget.error", + "translation": "No s'ha pogut publicar el missatge d'unir/deixar" + }, + { + "id": "api.channel.post_update_channel_header_message_and_forget.updated_to", + "translation": "%s ha actualitzat la capçalera del canal a: %s" + }, + { + "id": "api.channel.post_update_channel_header_message_and_forget.updated_from", + "translation": "%s ha actualitzat la capçalera del canal de: %s a: %s" + }, + { + "id": "api.channel.post_update_channel_header_message_and_forget.retrieve_user.error", + "translation": "No s'ha pogut recuperar l'usuari durant l'actualització de la capçalera del canal" + }, + { + "id": "api.channel.post_update_channel_header_message_and_forget.removed", + "translation": "%s ha eliminat la capçalera del canal (era: %s)" + }, + { + "id": "api.channel.post_update_channel_header_message_and_forget.post.error", + "translation": "No s'ha pogut publicar el missatge d'actualització de la capçalera del canal" + }, + { + "id": "api.channel.post_update_channel_displayname_message_and_forget.updated_from", + "translation": "%s ha actualitzat el nom del canal de: %s a: %s" + }, + { + "id": "api.channel.post_update_channel_displayname_message_and_forget.retrieve_user.error", + "translation": "No s'ha pogut recuperar l'usuari mentre s'actualitzava el camp «DisplayName» del canal" + }, + { + "id": "api.channel.post_update_channel_displayname_message_and_forget.create_post.error", + "translation": "No s'ha pogut publicar el missatge d'actualització del nom del canal" + }, + { + "id": "api.channel.post_channel_privacy_message.error", + "translation": "No s'ha pogut publicar el missatge d'actualització de la privadesa del canal." + }, + { + "id": "api.channel.patch_update_channel.forbidden.app_error", + "translation": "No s'ha pogut actualitzar el canal" + }, + { + "id": "api.channel.patch_channel_moderations.license.error", + "translation": "La teva llicència no és compatible amb la retenció de dades." + }, + { + "id": "api.channel.leave.left", + "translation": "%v ha deixat el canal." + }, + { + "id": "api.channel.leave.last_member.app_error", + "translation": "Ets l'únic membre, intenta eliminar el canal privat en lloc de sortir." + }, + { + "id": "api.channel.leave.direct.app_error", + "translation": "No es pot deixar un canal de missatge directe" + }, + { + "id": "api.channel.leave.default.app_error", + "translation": "No es pot deixar el canal per defecte {{.Channel}}" + }, + { + "id": "api.channel.join_channel.post_and_forget", + "translation": "%v s'ha unit al canal." + }, + { + "id": "api.channel.join_channel.permissions.app_error", + "translation": "No teniu els permisos apropiats" + }, + { + "id": "api.channel.guest_join_channel.post_and_forget", + "translation": "%v s'ha afegit al canal com a visitant." + }, + { + "id": "api.channel.get_channel_moderations.license.error", + "translation": "La teva llicència no és compatible amb la retenció de dades." + }, + { + "id": "api.channel.delete_channel.type.invalid", + "translation": "No es poden eliminar canals grupals o missatges directes" + }, + { + "id": "api.channel.delete_channel.deleted.app_error", + "translation": "El canal ha estat arxivat o suprimit" + }, + { + "id": "api.channel.delete_channel.cannot.app_error", + "translation": "No es pot eliminar el canal per defecte {{.Channel}}" + }, + { + "id": "api.channel.delete_channel.archived", + "translation": "%v ha arxivat el canal." + }, + { + "id": "api.channel.create_group.bad_user.app_error", + "translation": "Un dels usuaris proporcionats no existeix" + }, + { + "id": "api.channel.create_group.bad_size.app_error", + "translation": "Els canals de missatge grupals han de contenir com a mínim 3 i no més de 8 usuaris" + }, + { + "id": "api.channel.create_direct_channel.invalid_user.app_error", + "translation": "ID d'usuari no vàlid per a la creació de canals en directe" + }, + { + "id": "api.channel.create_default_channels.town_square", + "translation": "Plaça major" + }, + { + "id": "api.channel.create_default_channels.off_topic", + "translation": "Fora de tema" + }, + { + "id": "api.channel.create_channel.max_channel_limit.app_error", + "translation": "No es poden crear més de {{.MaxChannelsPerTeam}} canals per a l'equip actual" + }, + { + "id": "api.channel.create_channel.direct_channel.app_error", + "translation": "S'ha de fer servir el servei de l'API createDirectChannel per crear un canal de missatges privats" + }, + { + "id": "api.channel.channel_member_counts_by_group.license.error", + "translation": "La teva llicència no suporta: Idap groups" + }, + { + "id": "api.channel.change_channel_privacy.public_to_private", + "translation": "Aquest canal s'ha convertit a Canal Privat." + }, + { + "id": "api.channel.change_channel_privacy.private_to_public", + "translation": "Aquest canal s'ha convertit a Canal Públic i qualsevol membre de l'equip pot unir-se." + }, + { + "id": "api.channel.add_user_to_channel.type.app_error", + "translation": "No es pot afegir l'usuari en aquest tipus de canal" + }, + { + "id": "api.channel.add_user.to.channel.failed.deleted.app_error", + "translation": "No s'ha pogut afegir l'usuari al canal perquè ha estat eliminat de l'equip." + }, + { + "id": "api.channel.add_user.to.channel.failed.app_error", + "translation": "No s'ha pogut afegir l'usuari al canal" + }, + { + "id": "api.channel.add_members.user_denied", + "translation": "Pertinença al canal denegada als següents usuaris a causa de restriccions de grup: {{ .UserIDs }}" + }, + { + "id": "api.channel.add_members.error", + "translation": "Error al afegir persones al canal." + }, + { + "id": "api.channel.add_member.added", + "translation": "%v afegit al canal per %v" + }, + { + "id": "api.channel.add_guest.added", + "translation": "%v afegit al canal per %v" + }, + { + "id": "api.bot.teams_channels.add_message_mobile", + "translation": "Si us plau afegeix-me a l'equip i als canals als que vols que interactuí. Per fer això, fes servir el navegador o l'app d'escriptori de Mattermost." + }, + { + "id": "api.bot.create_disabled", + "translation": "La creació de bots ha estat deshabilitada." + }, + { + "id": "api.scheme.patch_scheme.license.error", + "translation": "La teva llicència no és compatible amb l'actualització d'esquemes de permisos" + }, + { + "id": "api.scheme.get_teams_for_scheme.scope.error", + "translation": "No es poden obtenir els equips per a l'esquema perquè l'esquema proporcionat no és un esquema d'equip." + }, + { + "id": "api.scheme.get_channels_for_scheme.scope.error", + "translation": "No es poden obtenir els canals d'esquema perquè l'esquema proporcionat no és un esquema de canal." + }, + { + "id": "api.scheme.delete_scheme.license.error", + "translation": "La teva llicència no és compatible amb l'eliminació d'esquemes de permisos" + }, + { + "id": "api.scheme.create_scheme.license.error", + "translation": "La teva llicència no és compatible amb la creació d'esquemes de permisos." + }, + { + "id": "api.roles.patch_roles.license.error", + "translation": "La teva llicència no és compatible amb els permisos avançats." + }, + { + "id": "api.restricted_system_admin", + "translation": "Aquesta acció està prohibida i s'ha restringit a administradors del sistema." + }, + { + "id": "api.reaction.save_reaction.user_id.app_error", + "translation": "No pots desar la reacció de l'altre usuari." + }, + { + "id": "api.reaction.save_reaction.invalid.app_error", + "translation": "Reacció no vàlida." + }, + { + "id": "api.reaction.save.archived_channel.app_error", + "translation": "No pots reaccionar en un canal arxivat." + }, + { + "id": "api.reaction.delete.archived_channel.app_error", + "translation": "No pots eliminar una reacció d'un canal arxivat." + }, + { + "id": "api.push_notifications_ack.message.parse.app_error", + "translation": "S'ha produït un error en crear el missatge ACK de notificació" + }, + { + "id": "api.push_notifications_ack.forward.app_error", + "translation": "S'ha produït un error en enviar la recepció de rebut al servei de Notificacions Push" + }, + { + "id": "api.push_notifications.message.parse.app_error", + "translation": "S'ha produït un error en crear el missatge de notificació" + }, + { + "id": "api.push_notification.id_loaded.fetch.app_error", + "translation": "S'ha produït un error en obtenir la notificació push carregada per ID" + }, + { + "id": "api.push_notification.id_loaded.default_message", + "translation": "Has rebut un nou missatge." + }, + { + "id": "api.push_notification.disabled.app_error", + "translation": "Les Notificacions Push estan deshabilitades en aquest servidor." + }, + { + "id": "api.preference.update_preferences.set.app_error", + "translation": "No s'ha pogut establir les preferències de l'usuari." + }, + { + "id": "api.preference.preferences_category.get.app_error", + "translation": "No s'ha pogut obtenir les preferències de l'usuari." + }, + { + "id": "api.preference.delete_preferences.delete.app_error", + "translation": "No s'ha pogut eliminar les preferències de l'usuari." + }, + { + "id": "api.post_get_post_by_id.get.app_error", + "translation": "No es pot obtenir la publicació" + }, + { + "id": "api.post.update_post.system_message.app_error", + "translation": "No s'ha pogut actualitzar el missatge del sistema" + }, + { + "id": "api.post.update_post.permissions_time_limit.app_error", + "translation": "Només es permet l'edició de publicacions durant {{.timeLimit}} segons. Pregunteu a l'administrador del sistema per obtenir més detalls." + }, + { + "id": "api.post.update_post.permissions_details.app_error", + "translation": "Ja s'ha eliminat id={{.PostId}}" + }, + { + "id": "api.post.update_post.find.app_error", + "translation": "No es pot trobar la publicació o comentari existent per actualitzar-ho." + }, + { + "id": "api.post.update_post.can_not_update_post_in_deleted.error", + "translation": "No es pot actualitzar una publicació en un canal suprimit." + }, + { + "id": "api.post.send_notifications_and_forget.push_message", + "translation": "t'ha enviat un missatge." + }, + { + "id": "api.post.send_notifications_and_forget.push_image_only", + "translation": " ha adjuntat un fitxer." + }, + { + "id": "api.admin.upload_brand_image.too_large.app_error", + "translation": "No s'ha pogut pujar el fitxer. El fitxer és massa gran." + }, + { + "id": "api.admin.upload_brand_image.storage.app_error", + "translation": "No s'ha pogut pujar la imatge. L'emmagatzematge d'imatges no està configurat." + }, + { + "id": "api.admin.upload_brand_image.parse.app_error", + "translation": "No s'ha pogut parsejar el formulari multipart" + }, + { + "id": "api.admin.upload_brand_image.no_file.app_error", + "translation": "Cap fitxer sota «imatge» a la petició" + }, + { + "id": "api.admin.upload_brand_image.array.app_error", + "translation": "Matriu buida sota 'image' a la petició" + }, + { + "id": "api.admin.test_s3.missing_s3_bucket", + "translation": "Es necessita S3 Bucket" + }, + { + "id": "api.admin.test_email.subject", + "translation": "Mattermost - Provant la configuració de correu" + }, + { + "id": "api.admin.test_email.reenter_password", + "translation": "El servidor SMTP, el seu port o el nom d'usuari ha canviat. Torneu a inserir la contrasenya del servidor SMTP per a provar la connexió." + }, + { + "id": "api.admin.test_email.missing_server", + "translation": "El servidor SMTP és obligatori" + }, + { + "id": "api.admin.test_email.body", + "translation": "Sembla que la configuració de correu del Mattermost és correcta!" + }, + { + "id": "api.admin.saml.set_certificate_from_metadata.missing_content_type.app_error", + "translation": "Falta el tipus de contingut." + }, + { + "id": "api.admin.saml.set_certificate_from_metadata.invalid_content_type.app_error", + "translation": "Tipus de contingut no vàlid." + }, + { + "id": "api.admin.saml.set_certificate_from_metadata.invalid_body.app_error", + "translation": "Text del certificat no vàlid." + }, + { + "id": "api.admin.saml.not_available.app_error", + "translation": "SAML 2.0 no està configurat o no és compatible en aquest servidor." + }, + { + "id": "api.admin.saml.metadata.app_error", + "translation": "S'ha produït un error en crear el Service Provider Metadata." + }, + { + "id": "api.admin.saml.invalid_xml_missing_ssoservices.app_error", + "translation": "Falta el node dels Serveis SSO en el XML del Proveïdor d'Identitat" + }, + { + "id": "api.admin.saml.invalid_xml_missing_keydescriptor.app_error", + "translation": "Falta el node de les claus dels descriptors en el XML del Proveïdor d'Identitat" + }, + { + "id": "api.admin.saml.invalid_xml_missing_idpssodescriptors.app_error", + "translation": "Falta el node dels descriptors SSO a l'XML del proveïdor d'identitat" + }, + { + "id": "api.admin.saml.failure_save_idp_certificate_file.app_error", + "translation": "No s'ha pogut desar el certificat." + }, + { + "id": "api.admin.saml.failure_parse_idp_certificate.app_error", + "translation": "S'ha produït un error mentre es transferia la informació de les metadades rebudes del proveïdor d'identitat a un certificat." + }, + { + "id": "api.admin.saml.failure_get_metadata_from_idp.app_error", + "translation": "S'ha produït un error en obtenir les metadades des de la URL del proveïdor d'identitat" + }, + { + "id": "api.admin.remove_certificate.delete.app_error", + "translation": "S'ha produït un error en suprimir el certificat." + }, + { + "id": "api.admin.get_brand_image.storage.app_error", + "translation": "No s'ha configurat l'emmagatzemament d'imatges." + }, + { + "id": "api.admin.file_read_error", + "translation": "S'ha produït un error en llegir el fitxer de registre." + }, + { + "id": "api.admin.delete_brand_image.storage.not_found", + "translation": "No es pot esborrar la imatge de marca, no trobada." + }, + { + "id": "api.admin.add_certificate.saving.app_error", + "translation": "No s'ha pogut desar el fitxer del certificat." + }, + { + "id": "api.admin.add_certificate.open.app_error", + "translation": "No s'ha pogut obrir el fitxer del certificat." + }, + { + "id": "api.admin.add_certificate.no_file.app_error", + "translation": "Cap fitxer sota certificat a la petició." + }, + { + "id": "api.admin.add_certificate.array.app_error", + "translation": "Cap fitxer sota «certificat» a la petició." + }, + { + "id": "September", + "translation": "Setembre" + }, + { + "id": "October", + "translation": "Octubre" + }, + { + "id": "November", + "translation": "Novembre" + }, + { + "id": "May", + "translation": "Maig" + }, + { + "id": "March", + "translation": "Març" + }, + { + "id": "June", + "translation": "Juny" + }, + { + "id": "July", + "translation": "Juliol" + }, + { + "id": "January", + "translation": "Gener" + }, + { + "id": "February", + "translation": "Febrer" + }, + { + "id": "December", + "translation": "Desembre" + }, + { + "id": "August", + "translation": "Agost" + }, + { + "id": "April", + "translation": "Abril" + }, + { + "id": "model.group.name.invalid_length.app_error", + "translation": "El nom ha de tenir entre 1 i 64 caràcters alfanumèrics en minúscula" + } +] diff --git a/server/i18n/cs.json b/server/i18n/cs.json new file mode 100644 index 00000000000..1306b152e83 --- /dev/null +++ b/server/i18n/cs.json @@ -0,0 +1,7490 @@ +[ + { + "id": "web.incoming_webhook.user.app_error", + "translation": "Nemohu najít uživatele." + }, + { + "id": "web.incoming_webhook.text.app_error", + "translation": "Nebyl zadán text." + }, + { + "id": "web.incoming_webhook.permissions.app_error", + "translation": "Nedostatečná oprávnění ke kanálu." + }, + { + "id": "web.incoming_webhook.parse.app_error", + "translation": "Neporozuměl jsem příchozím datům." + }, + { + "id": "web.incoming_webhook.invalid.app_error", + "translation": "Nevalidní webhook." + }, + { + "id": "web.incoming_webhook.disabled.app_error", + "translation": "Příkazy byly zakázány systémovým administrátorem." + }, + { + "id": "web.incoming_webhook.channel.app_error", + "translation": "Nemohl jsem najít daný kanál." + }, + { + "id": "web.get_access_token.internal_saving.app_error", + "translation": "Nepodařilo se aktualizovat uživatelova přístupová data." + }, + { + "id": "web.command_webhook.parse.app_error", + "translation": "Neporozuměl jsem příchozím datům." + }, + { + "id": "web.command_webhook.command.app_error", + "translation": "Tento příkaz nelze najít." + }, + { + "id": "system.message.name", + "translation": "Systém" + }, + { + "id": "store.sql_user.get_for_login.app_error", + "translation": "Nelze najít existující účet odpovídající vašim pověřením. Tento tým může vyžadovat pozvání od vlastníka týmu, aby se připojil." + }, + { + "id": "store.sql_command.save.get.app_error", + "translation": "Nepodařilo se získat příkaz." + }, + { + "id": "app.channel.update.bad_id", + "translation": "Nepodařilo se aktualizovat kanál." + }, + { + "id": "store.sql_channel.save_channel.exists.app_error", + "translation": "Kanál s tímto jménem pro stejný tým již existuje." + }, + { + "id": "store.sql_channel.save_channel.existing.app_error", + "translation": "Musí být vyvolána aktualizace pro existující kanál." + }, + { + "id": "store.sql_channel.save.direct_channel.app_error", + "translation": "Pokud chcete vytvořit přímý kanál použijte SaveDirectChannel." + }, + { + "id": "store.sql_channel.save.archived_channel.app_error", + "translation": "Nelze upravit archivovaný kanál." + }, + { + "id": "app.channel.get_deleted.existing.app_error", + "translation": "Nelze najít existující smazaný kanál." + }, + { + "id": "app.channel.get_channels.not_found.app_error", + "translation": "Žádné kanály nebyly nalezeny." + }, + { + "id": "app.channel.get_by_name.missing.app_error", + "translation": "Kanál neexistuje." + }, + { + "id": "store.sql_channel.get.existing.app_error", + "translation": "Nelze najít daný kanál." + }, + { + "id": "app.audit.save.saving.app_error", + "translation": "Došlo k chybě při ukládání auditních záznamů." + }, + { + "id": "app.audit.permanent_delete_by_user.app_error", + "translation": "Došlo k chybě při mazání auditních záznamů." + }, + { + "id": "app.audit.get.finding.app_error", + "translation": "Došlo k chybě při hledání audit záznamů." + }, + { + "id": "plugin_api.get_file_link.no_post.app_error", + "translation": "K souboru nelze zajistit veřejný odkaz. Soubor musí být přiložen ke zprávě, která je čitelná aktuálnímu uživateli." + }, + { + "id": "plugin_api.get_file_link.disabled.app_error", + "translation": "Vkládání veřejných odkazů není povoleno." + }, + { + "id": "oauth.gitlab.tos.error", + "translation": "Podmínky užívání GitLabu byly změněny. Prosím přihlašte se na gitlab.com, akceptujte je a potom se zkuste přihlásit do Mattermostu znovu." + }, + { + "id": "model.utils.decode_json.app_error", + "translation": "nelze dekódovat." + }, + { + "id": "model.user_access_token.is_valid.user_id.app_error", + "translation": "Neplatné uživatelské id." + }, + { + "id": "model.user_access_token.is_valid.token.app_error", + "translation": "Neplatný přístupový token." + }, + { + "id": "model.user_access_token.is_valid.id.app_error", + "translation": "Neplatná hodnota pro id." + }, + { + "id": "model.user.is_valid.username.app_error", + "translation": "Uživatelské jméno musí začínat písmenem a obsahovat 3 až 22 znakú, včetně čísel, malých písmen a symbolů \".\", \"-\" a \"_\"." + }, + { + "id": "model.user.is_valid.update_at.app_error", + "translation": "Čas aktualizace musí být platný časový údaj." + }, + { + "id": "model.user.is_valid.pwd_uppercase_symbol.app_error", + "translation": "Vaše heslo musí obsahovat nejméně {{.Min}} znaky složené z alespoň jednoho malého písmena a alespoň jednoho symbolu (např. \"~!@#$%^&*()\")." + }, + { + "id": "model.user.is_valid.pwd_uppercase_number_symbol.app_error", + "translation": "Heslo musí obsahovat nejméně {{.Min}} znaky, které obsahují alespoň jedno malé písmeno, alespoň jedno číslo a alespoň jeden symbol (např. \"~!@#$%^&*()\")." + }, + { + "id": "model.user.is_valid.pwd_uppercase_number.app_error", + "translation": "Vaše heslo musí obsahovat nejméně {{.Min}} znaky tvořené alespoň jedním malým písmenem a alespoň jedním velkým písmenem." + }, + { + "id": "model.user.is_valid.pwd_uppercase.app_error", + "translation": "Vaše heslo musí obsahovat nejméně {{.Min}} znaky složené z alespoň jednoho čísla." + }, + { + "id": "model.user.is_valid.pwd_symbol.app_error", + "translation": "Vaše heslo musí obsahovat nejméně {{.Min}} znaky složené z alespoň jednoho symbolu (např. \"~!@#$%^&*()\")." + }, + { + "id": "model.user.is_valid.pwd_number_symbol.app_error", + "translation": "Vaše heslo musí obsahovat nejméně {{.Min}} znaky složené z alespoň jednoho malého písmena a alespoň jednoho symbolu (např. \"~!@#$%^&*()\")." + }, + { + "id": "model.user.is_valid.pwd_number.app_error", + "translation": "Vaše heslo musí obsahovat nejméně {{.Min}} znaky složené z alespoň jednoho čísla." + }, + { + "id": "model.user.is_valid.pwd_lowercase_uppercase_symbol.app_error", + "translation": "Vaše heslo musí obsahovat nejméně {{.Min}} znaky složené z alespoň jednoho malého písmena, alespoň jednoho velkého písmena a alespoň jednoho symbolu (např. \"~!@#$%^&*()\")." + }, + { + "id": "model.user.is_valid.pwd_lowercase_uppercase_number_symbol.app_error", + "translation": "Vaše heslo musí obsahovat nejméně {{.Min}} znaky složené z alespoň jednoho malého písmena, alespoň jednoho velkého písmena, alespoň jednoho čísla a alespoň jednoho symbolu (např. \"~!@#$%^&*()\")." + }, + { + "id": "model.user.is_valid.pwd_lowercase_uppercase_number.app_error", + "translation": "Vaše heslo musí obsahovat nejméně {{.Min}} znaky složené z alespoň jednoho malého písmena, alespoň jednoho velkého písmena a alespoň jednoho symbolu (např. \"~!@#$%^&*()\")." + }, + { + "id": "model.user.is_valid.pwd_lowercase_uppercase.app_error", + "translation": "Vaše heslo musí obsahovat nejméně {{.Min}} znaky tvořené alespoň jedním malým písmenem a alespoň jedním velkým písmenem." + }, + { + "id": "model.user.is_valid.pwd_lowercase_symbol.app_error", + "translation": "Vaše heslo musí obsahovat nejméně {{.Min}} znaky složené z alespoň jednoho malého písmena a alespoň jednoho symbolu (např. \"~!@#$%^&*()\")." + }, + { + "id": "model.user.is_valid.pwd_lowercase_number_symbol.app_error", + "translation": "Heslo musí obsahovat nejméně {{.Min}} znaky, které obsahují alespoň jedno malé písmeno, alespoň jedno číslo a alespoň jeden symbol (např. \"~!@#$%^&*()\")." + }, + { + "id": "model.user.is_valid.pwd_lowercase_number.app_error", + "translation": "Vaše heslo musí obsahovat nejméně {{.Min}} znaky tvořené alespoň jedním malým písmenem a alespoň jedním velkým písmenem." + }, + { + "id": "model.user.is_valid.pwd_lowercase.app_error", + "translation": "Vaše heslo musí obsahovat nejméně {{.Min}} znaky složené z alespoň jednoho čísla." + }, + { + "id": "model.user.is_valid.pwd.app_error", + "translation": "Vaše heslo musí obsahovat nejmíň {{.Min}} znaků." + }, + { + "id": "model.user.is_valid.position.app_error", + "translation": "Neplatná pozice: nesmí být delší než 128 znaků." + }, + { + "id": "model.user.is_valid.nickname.app_error", + "translation": "Nevalidní přezdívka." + }, + { + "id": "model.user.is_valid.locale.app_error", + "translation": "Nevalidní locale." + }, + { + "id": "model.user.is_valid.last_name.app_error", + "translation": "Nevalidní příjmení." + }, + { + "id": "model.user.is_valid.id.app_error", + "translation": "Neplatné uživatelské id." + }, + { + "id": "model.user.is_valid.first_name.app_error", + "translation": "Nevalidní křestní jméno." + }, + { + "id": "model.user.is_valid.email.app_error", + "translation": "Neplatná e-mailová adresa." + }, + { + "id": "model.user.is_valid.create_at.app_error", + "translation": "Čas vytvoření musí být platný časový údaj." + }, + { + "id": "model.user.is_valid.auth_data_pwd.app_error", + "translation": "Neplatné uživatelské jméno, heslo a autentizační data nemohou být nastavena." + }, + { + "id": "model.user.is_valid.auth_data.app_error", + "translation": "Neplatná autentizační data." + }, + { + "id": "model.token.is_valid.size", + "translation": "Neplatný token." + }, + { + "id": "model.token.is_valid.expiry", + "translation": "Neplatné token expiry" + }, + { + "id": "model.team_member.is_valid.user_id.app_error", + "translation": "Neplatné uživatelské id." + }, + { + "id": "model.team_member.is_valid.team_id.app_error", + "translation": "Neplatné ID týmu." + }, + { + "id": "model.team.is_valid.update_at.app_error", + "translation": "Čas aktualizace musí být platný časový údaj." + }, + { + "id": "model.team.is_valid.type.app_error", + "translation": "Neplatný typ." + }, + { + "id": "model.team.is_valid.reserved.app_error", + "translation": "Tato adresa URL není k dispozici. Zkuste prosím jinou." + }, + { + "id": "model.team.is_valid.name.app_error", + "translation": "Neplatné jméno." + }, + { + "id": "model.team.is_valid.id.app_error", + "translation": "Nevalidní Id." + }, + { + "id": "model.team.is_valid.email.app_error", + "translation": "Neplatná e-mailová adresa." + }, + { + "id": "model.team.is_valid.domains.app_error", + "translation": "Neplatné povolené domény." + }, + { + "id": "model.team.is_valid.description.app_error", + "translation": "Neplatný popis." + }, + { + "id": "model.team.is_valid.create_at.app_error", + "translation": "Čas vytvoření musí být platný časový údaj." + }, + { + "id": "model.team.is_valid.company.app_error", + "translation": "Nevalidní název společnosti." + }, + { + "id": "model.team.is_valid.characters.app_error", + "translation": "Název musí obsahovat 2 nebo více malých alfanumerických znaků." + }, + { + "id": "model.reaction.is_valid.user_id.app_error", + "translation": "Neplatné uživatelské id." + }, + { + "id": "model.reaction.is_valid.post_id.app_error", + "translation": "Neplatné ID příspěvku." + }, + { + "id": "model.reaction.is_valid.emoji_name.app_error", + "translation": "Nevalidní název emoji." + }, + { + "id": "model.reaction.is_valid.create_at.app_error", + "translation": "Čas vytvoření musí být platný časový údaj." + }, + { + "id": "model.preference.is_valid.value.app_error", + "translation": "Hodnota je příliš dlouhá." + }, + { + "id": "model.preference.is_valid.theme.app_error", + "translation": "Neplatný motiv." + }, + { + "id": "model.preference.is_valid.name.app_error", + "translation": "Neplatné jméno." + }, + { + "id": "model.preference.is_valid.id.app_error", + "translation": "Neplatné uživatelské id." + }, + { + "id": "model.preference.is_valid.category.app_error", + "translation": "Nevalidní kategorie." + }, + { + "id": "model.post.is_valid.user_id.app_error", + "translation": "Neplatné uživatelské id." + }, + { + "id": "model.post.is_valid.update_at.app_error", + "translation": "Čas aktualizace musí být platný časový údaj." + }, + { + "id": "model.post.is_valid.type.app_error", + "translation": "Neplatný typ." + }, + { + "id": "model.post.is_valid.root_id.app_error", + "translation": "Neplatné ID roota." + }, + { + "id": "model.post.is_valid.props.app_error", + "translation": "Nevalidní položky." + }, + { + "id": "model.post.is_valid.original_id.app_error", + "translation": "Neplatné původní ID." + }, + { + "id": "model.post.is_valid.msg.app_error", + "translation": "Neplatná zpráva." + }, + { + "id": "model.post.is_valid.id.app_error", + "translation": "Nevalidní Id." + }, + { + "id": "model.post.is_valid.hashtags.app_error", + "translation": "Neplatné hashtagy." + }, + { + "id": "model.post.is_valid.filenames.app_error", + "translation": "Neplatné názvy souborů." + }, + { + "id": "model.post.is_valid.file_ids.app_error", + "translation": "Neplatné ID souborů. Maximálně je možné zároveň nahrávat 5 souborů. Pokud potřebujete víc, vytvořte další příspěvky." + }, + { + "id": "model.post.is_valid.create_at.app_error", + "translation": "Čas vytvoření musí být platný časový údaj." + }, + { + "id": "model.post.is_valid.channel_id.app_error", + "translation": "Neplatné id kanálu." + }, + { + "id": "model.outgoing_hook.username.app_error", + "translation": "Neplatné uživatelské jméno." + }, + { + "id": "model.outgoing_hook.is_valid.words.app_error", + "translation": "Neplatná spouštěcí slova." + }, + { + "id": "model.outgoing_hook.is_valid.user_id.app_error", + "translation": "Neplatné uživatelské id." + }, + { + "id": "model.outgoing_hook.is_valid.url.app_error", + "translation": "Neplatná adresa URL zpětného volání. Každá musí být platná adresa URL a začít http:// nebo https://." + }, + { + "id": "model.outgoing_hook.is_valid.update_at.app_error", + "translation": "Čas aktualizace musí být platný časový údaj." + }, + { + "id": "model.outgoing_hook.is_valid.trigger_words.app_error", + "translation": "Neplatná spouštěcí slova." + }, + { + "id": "model.outgoing_hook.is_valid.token.app_error", + "translation": "Neplatný token." + }, + { + "id": "model.outgoing_hook.is_valid.team_id.app_error", + "translation": "Neplatné ID týmu." + }, + { + "id": "model.outgoing_hook.is_valid.id.app_error", + "translation": "Nevalidní Id." + }, + { + "id": "model.outgoing_hook.is_valid.display_name.app_error", + "translation": "Neplatný název." + }, + { + "id": "model.outgoing_hook.is_valid.description.app_error", + "translation": "Neplatný popis." + }, + { + "id": "model.outgoing_hook.is_valid.create_at.app_error", + "translation": "Čas vytvoření musí být platný časový údaj." + }, + { + "id": "model.outgoing_hook.is_valid.channel_id.app_error", + "translation": "Neplatné id kanálu." + }, + { + "id": "model.outgoing_hook.icon_url.app_error", + "translation": "Nevalidní ikona." + }, + { + "id": "model.oauth.is_valid.update_at.app_error", + "translation": "Čas aktualizace musí být platný časový údaj." + }, + { + "id": "model.oauth.is_valid.name.app_error", + "translation": "Neplatné jméno." + }, + { + "id": "model.oauth.is_valid.icon_url.app_error", + "translation": "Adresa URL ikony musí být platná adresa URL a začít http:// nebo https://." + }, + { + "id": "model.oauth.is_valid.homepage.app_error", + "translation": "Domovská stránka musí být platná adresa URL a začít s http:// nebo https://." + }, + { + "id": "model.oauth.is_valid.description.app_error", + "translation": "Neplatný popis." + }, + { + "id": "model.oauth.is_valid.creator_id.app_error", + "translation": "Neplatné ID autora." + }, + { + "id": "model.oauth.is_valid.create_at.app_error", + "translation": "Čas vytvoření musí být platný časový údaj." + }, + { + "id": "model.oauth.is_valid.client_secret.app_error", + "translation": "Neplatné klientské tajemství." + }, + { + "id": "model.oauth.is_valid.callback.app_error", + "translation": "Adresa URL ikony musí být platná adresa URL a začít http:// nebo https://." + }, + { + "id": "model.oauth.is_valid.app_id.app_error", + "translation": "Neplatné id aplikace." + }, + { + "id": "model.license_record.is_valid.id.app_error", + "translation": "Neplatná hodnota create_at počas nahrávání licence." + }, + { + "id": "model.license_record.is_valid.create_at.app_error", + "translation": "Neplatná hodnota create_at počas nahrávání licence." + }, + { + "id": "model.job.is_valid.type.app_error", + "translation": "Nevalidní typ úlohy." + }, + { + "id": "model.job.is_valid.id.app_error", + "translation": "Nevalidní id úlohy." + }, + { + "id": "model.job.is_valid.create_at.app_error", + "translation": "Čas vytvoření musí být platný časový údaj." + }, + { + "id": "model.incoming_hook.username.app_error", + "translation": "Neplatné uživatelské jméno." + }, + { + "id": "model.incoming_hook.user_id.app_error", + "translation": "Neplatné uživatelské id." + }, + { + "id": "model.incoming_hook.update_at.app_error", + "translation": "Čas aktualizace musí být platný časový údaj." + }, + { + "id": "model.incoming_hook.team_id.app_error", + "translation": "Neplatné ID týmu." + }, + { + "id": "model.incoming_hook.parse_data.app_error", + "translation": "Neporozuměl jsem příchozím datům." + }, + { + "id": "model.incoming_hook.id.app_error", + "translation": "Nevalidní Id." + }, + { + "id": "model.incoming_hook.icon_url.app_error", + "translation": "Nevalidní ikona příspěvku." + }, + { + "id": "model.incoming_hook.display_name.app_error", + "translation": "Neplatný název." + }, + { + "id": "model.incoming_hook.description.app_error", + "translation": "Neplatný popis." + }, + { + "id": "model.incoming_hook.create_at.app_error", + "translation": "Čas vytvoření musí být platný časový údaj." + }, + { + "id": "model.incoming_hook.channel_id.app_error", + "translation": "Neplatné id kanálu." + }, + { + "id": "model.guest.is_valid.emails.app_error", + "translation": "Neplatné e-mailové adresy." + }, + { + "id": "model.guest.is_valid.email.app_error", + "translation": "Neplatná e-mailová adresa." + }, + { + "id": "model.guest.is_valid.channels.app_error", + "translation": "Neplatné kanály." + }, + { + "id": "model.guest.is_valid.channel.app_error", + "translation": "Neplatný kanál." + }, + { + "id": "model.file_info.is_valid.user_id.app_error", + "translation": "Neplatná hodnota pro user_id." + }, + { + "id": "model.file_info.is_valid.update_at.app_error", + "translation": "Neplatná hodnota pro update_at." + }, + { + "id": "model.file_info.is_valid.post_id.app_error", + "translation": "Neplatná hodnota pro post_id." + }, + { + "id": "model.file_info.is_valid.path.app_error", + "translation": "Neplatná hodnota pro path." + }, + { + "id": "model.file_info.is_valid.id.app_error", + "translation": "Neplatná hodnota pro id." + }, + { + "id": "model.file_info.is_valid.create_at.app_error", + "translation": "Neplatná hodnota pro create_at." + }, + { + "id": "model.file_info.get.gif.app_error", + "translation": "Nelze dekódovat soubor GIF." + }, + { + "id": "model.emoji.user_id.app_error", + "translation": "Neplatné ID autora." + }, + { + "id": "model.emoji.update_at.app_error", + "translation": "Čas aktualizace musí být platný časový údaj." + }, + { + "id": "model.emoji.name.app_error", + "translation": "Název musí obsahovat 1 až 64 malých alfanumerických znaků." + }, + { + "id": "model.emoji.id.app_error", + "translation": "Nevalidní id pro emoji." + }, + { + "id": "model.emoji.create_at.app_error", + "translation": "Čas vytvoření musí být platný časový údaj." + }, + { + "id": "model.config.is_valid.write_timeout.app_error", + "translation": "Neplatná hodnota pro časový limit zápisu." + }, + { + "id": "model.config.is_valid.websocket_url.app_error", + "translation": "Adresa URL webu musí být platná adresa URL a začínat ws:// nebo wss://." + }, + { + "id": "model.config.is_valid.time_between_user_typing.app_error", + "translation": "Čas mezi aktualizacemi psaní uživatele by neměl být nastaven na méně než 1000 milisekund." + }, + { + "id": "model.config.is_valid.sql_query_timeout.app_error", + "translation": "Neplatná hodnota časového limitu spojení v nastavení SQL. Hodnota musí být kladné číslo." + }, + { + "id": "model.config.is_valid.sql_max_conn.app_error", + "translation": "Neplatné hodnota nastavení maximálního počtu kanálů pro týmy. Musí být kladné číslo." + }, + { + "id": "model.config.is_valid.sql_idle.app_error", + "translation": "Neplatná hodnota nejvyššího počtu nečinných spojení v nastavení SQL. Hodnota musí být kladné číslo." + }, + { + "id": "model.config.is_valid.sql_driver.app_error", + "translation": "Neplatný název ovladače pro SQL windows. Musí být 'mysql' nebo 'postgres'." + }, + { + "id": "model.config.is_valid.sql_data_src.app_error", + "translation": "Chybný zdroj dat v nastavení SQL. Musí být nastaven." + }, + { + "id": "model.config.is_valid.sitename_length.app_error", + "translation": "Název webu smí obsahovat maximálně {{.MaxLength}} znaků." + }, + { + "id": "model.config.is_valid.site_url_email_batching.app_error", + "translation": "Nelze povolit dávkování e-mailů, pokud není nastaven soubor SiteURL." + }, + { + "id": "model.config.is_valid.site_url.app_error", + "translation": "Adresa URL ikony musí být platná adresa URL a začít http:// nebo https://." + }, + { + "id": "model.config.is_valid.saml_username_attribute.app_error", + "translation": "Neplatný atribut Username. Musí být nastaven." + }, + { + "id": "model.config.is_valid.saml_public_cert.app_error", + "translation": "Chybí veřejný certifikát poskytovatele služeb. Zapomněli jste ho nahrát?" + }, + { + "id": "model.config.is_valid.saml_private_key.app_error", + "translation": "Chybějící soukromý klíč poskytovatele služeb. Zapomněli jste ho nahrát?" + }, + { + "id": "model.config.is_valid.saml_idp_url.app_error", + "translation": "SAML SSO URL musí být platná URL adresa a začínat na http:// nebo https://." + }, + { + "id": "model.config.is_valid.saml_idp_descriptor_url.app_error", + "translation": "Adresa URL pro přihlášení k poskytovateli služeb musí být platná adresa URL a začít http:// nebo https://." + }, + { + "id": "model.config.is_valid.saml_idp_cert.app_error", + "translation": "Chybějící veřejný klíč Identity Providera. Zapomněli jste ho nahrát?" + }, + { + "id": "model.config.is_valid.saml_email_attribute.app_error", + "translation": "Neplatný atribut emailu. Musí být nastaveno." + }, + { + "id": "model.config.is_valid.saml_canonical_algorithm.app_error", + "translation": "Neplatný kanonický algoritmus." + }, + { + "id": "model.config.is_valid.saml_assertion_consumer_service_url.app_error", + "translation": "Adresa URL pro přihlášení k poskytovateli služeb musí být platná adresa URL a začít http:// nebo https://." + }, + { + "id": "model.config.is_valid.restrict_direct_message.app_error", + "translation": "Neplatné omezení přímé zprávy. Musí být \"any\" nebo \"team\"." + }, + { + "id": "model.config.is_valid.read_timeout.app_error", + "translation": "Neplatná hodnota časového limitu čtení." + }, + { + "id": "model.config.is_valid.rate_sec.app_error", + "translation": "Neplatné nastavení velikosti úložiště pro omezení rychlosti. Musí být kladné číslo." + }, + { + "id": "model.config.is_valid.rate_mem.app_error", + "translation": "Neplatná velikost úložiště paměti pro nastavení limitu rychlosti. Musí to být kladné číslo." + }, + { + "id": "model.config.is_valid.password_length.app_error", + "translation": "Minimální délka hesla musí být celé číslo větší nebo rovné {{.MinLength}} a menší nebo rovno {{.MaxLength}}." + }, + { + "id": "model.config.is_valid.message_export.global_relay.smtp_username.app_error", + "translation": "Úloha exportu zpráv GlobalRelaySettings.SmtpUsername musí být nastaveno." + }, + { + "id": "model.config.is_valid.message_export.global_relay.smtp_password.app_error", + "translation": "Úloha exportu zpráv GlobalRelaySettings.SmtpPassword musí být nastaveno." + }, + { + "id": "model.config.is_valid.message_export.global_relay.email_address.app_error", + "translation": "Úloha exportu zpráv GlobalRelaySettings.EmailAddress musí mít nastavenou platnou emailovou adresu." + }, + { + "id": "model.config.is_valid.message_export.global_relay.customer_type.app_error", + "translation": "Export zpráv GlobalRelaySettings.CustomerType musí být nastave na 'A9' nebo 'A10'." + }, + { + "id": "model.config.is_valid.message_export.global_relay.config_missing.app_error", + "translation": "Úloha exportu zpráv ExportFormat je nastavena na 'globalrelay', ale GlobalRelaySettings nejsou nastaveny." + }, + { + "id": "model.config.is_valid.message_export.export_type.app_error", + "translation": "Úloha pro export zpráv ExportFormat musí být jedna z následujících 'actiance', 'csv' nebo'globalrelay'." + }, + { + "id": "ent.elasticsearch.start.create_bulk_processor_failed.app_error", + "translation": "Nepodařilo se vytvořit Elasticsearch dávkový procesor." + }, + { + "id": "ent.elasticsearch.start.already_started.app_error", + "translation": "Elasticsearch už běží." + }, + { + "id": "ent.elasticsearch.search_users.unmarshall_user_failed", + "translation": "Nepodařilo se dekódovat výsledky hledání" + }, + { + "id": "ent.elasticsearch.search_users.search_failed", + "translation": "Vyhledávání se nezdařilo" + }, + { + "id": "ent.elasticsearch.search_posts.unmarshall_post_failed", + "translation": "Nepodařilo se dekódovat výsledky hledání" + }, + { + "id": "ent.elasticsearch.search_posts.search_failed", + "translation": "Vyhledávání se nezdařilo" + }, + { + "id": "ent.elasticsearch.search_posts.parse_matches_failed", + "translation": "Nepodařilo se přečíst výsledky hledání" + }, + { + "id": "ent.elasticsearch.search_posts.disabled", + "translation": "Vyhledávání pomocí Elasticsearch není a tomto serveru povoleno" + }, + { + "id": "ent.elasticsearch.search_channels.unmarshall_channel_failed", + "translation": "Nepodařilo se dekódovat výsledky hledání" + }, + { + "id": "ent.elasticsearch.search_channels.search_failed", + "translation": "Vyhledávání se nezdařilo" + }, + { + "id": "ent.elasticsearch.search_channels.disabled", + "translation": "Vyhledávání pomocí Elasticsearch není na tomto serveru povoleno" + }, + { + "id": "ent.elasticsearch.refresh_indexes.refresh_failed", + "translation": "Nepodařilo se načíst Elasticsearch indexy" + }, + { + "id": "ent.elasticsearch.purge_indexes.delete_failed", + "translation": "Nepodařilo se smazat index pro Elasticsearch" + }, + { + "id": "ent.elasticsearch.not_started.error", + "translation": "Elasticsearch neběží" + }, + { + "id": "ent.elasticsearch.indexer.index_batch.nothing_left_to_index.error", + "translation": "Zkouším vytvořit index pro novou dávku, když jsou všechny entity dokončené" + }, + { + "id": "ent.elasticsearch.indexer.do_job.parse_start_time.error", + "translation": "Indexovacímu procesu Elasticsearch se nepodařilo pročíst čas začátku" + }, + { + "id": "ent.elasticsearch.indexer.do_job.parse_end_time.error", + "translation": "Indexovacímu procesu Elasticsearch se nepodařilo pročíst čas konce" + }, + { + "id": "ent.elasticsearch.index_user.error", + "translation": "Nepodařilo se vytvořit index uživatele" + }, + { + "id": "ent.elasticsearch.index_post.error", + "translation": "Nepodařilo se vytvořit index příspěvků" + }, + { + "id": "ent.elasticsearch.index_channel.error", + "translation": "Nepodařilo vytvořit index kanálu" + }, + { + "id": "ent.elasticsearch.generic.disabled", + "translation": "Vyhledávání pomocí Elasticsearch není povoleno na tomto serveru" + }, + { + "id": "ent.elasticsearch.delete_user.error", + "translation": "Nepodařilo se smazat uživatele" + }, + { + "id": "ent.elasticsearch.delete_post.error", + "translation": "Chyba při mazání příspěvku" + }, + { + "id": "ent.elasticsearch.delete_channel.error", + "translation": "Nepodařilo se smazat kanál" + }, + { + "id": "ent.elasticsearch.data_retention_delete_indexes.get_indexes.error", + "translation": "Nepodařilo se načíst Elasticsearch indexy" + }, + { + "id": "ent.elasticsearch.data_retention_delete_indexes.delete_index.error", + "translation": "Nepodařilo se smazat Elasticsearch index" + }, + { + "id": "ent.elasticsearch.create_template_users_if_not_exists.template_create_failed", + "translation": "Selhala tvorba šablony Elasticsearch pro uživatele" + }, + { + "id": "ent.elasticsearch.create_template_posts_if_not_exists.template_create_failed", + "translation": "Selhala tvorba Elasticsearch šablony pro příspěvky" + }, + { + "id": "ent.elasticsearch.create_template_channels_if_not_exists.template_create_failed", + "translation": "Selhala tvoba šablony Elasticsearch pro kanály" + }, + { + "id": "ent.elasticsearch.create_client.connect_failed", + "translation": "Nastavení Elasticsearch klienta selhalo" + }, + { + "id": "ent.elasticsearch.aggregator_worker.index_job_failed.error", + "translation": "Agregátor Elasticsearch selhal protože indexovací úloha selhala" + }, + { + "id": "ent.elasticsearch.aggregator_worker.get_indexes.error", + "translation": "Agregátoru Elasticsearch se nezdařilo načíst indexy" + }, + { + "id": "ent.elasticsearch.aggregator_worker.delete_indexes.error", + "translation": "Agregátoru Elasticsearch se nezdařilo smazat indexy" + }, + { + "id": "ent.elasticsearch.aggregator_worker.create_index_job.error", + "translation": "Agregátoru Elasticsearch se nezdařilo vytvoření indexovací úlohy" + }, + { + "id": "ent.data_retention.generic.license.error", + "translation": "Vaše licence nepodporuje uchování dat." + }, + { + "id": "ent.compliance.run_failed.error", + "translation": "Pro úlohu '{{.JobName}}' v '{{.FilePath}}' se nezdařil export" + }, + { + "id": "ent.compliance.run_export.template_watcher.appError", + "translation": "Nepodařilo se načíst šablony exportu. Prosím zkuste to znova." + }, + { + "id": "ent.compliance.licence_disable.app_error", + "translation": "Funkce splnění požadavků je zakázána aktuální licencí. Obraťte se na správce systému o inovaci podnikové licence." + }, + { + "id": "ent.compliance.global_relay.rewind_temporary_file.appError", + "translation": "Nepodařilo se přečíst dočasný soubor Global Relay exportu." + }, + { + "id": "ent.compliance.global_relay.open_temporary_file.appError", + "translation": "Nepodařilo se otevřít dočasný soubor exportu." + }, + { + "id": "ent.compliance.global_relay.attachments_removed.appError", + "translation": "Nahraný soubor byl odstraněn z exportu Global Relay protože byl moc velký." + }, + { + "id": "ent.compliance.csv.zip.creation.appError", + "translation": "Nepodařilo se vytvořit zip soubor pro export." + }, + { + "id": "ent.compliance.csv.warning.appError", + "translation": "Nepodařilo se vytvořit eml soubor." + }, + { + "id": "ent.compliance.csv.post.export.appError", + "translation": "Nepodařilo se exportovat příspěvek." + }, + { + "id": "ent.compliance.csv.metadata.json.zipfile.appError", + "translation": "Nepodařilo se vytvořit zip soubor" + }, + { + "id": "ent.compliance.csv.metadata.json.marshalling.appError", + "translation": "Nepodařilo se zkonvertovat metadata do JSON." + }, + { + "id": "ent.compliance.csv.metadata.export.appError", + "translation": "Nepodařilo se přidat metadata soubor do zip souboru." + }, + { + "id": "ent.compliance.csv.header.export.appError", + "translation": "Nepodařilo se přidat hlavičku do CSV exportu." + }, + { + "id": "ent.compliance.csv.file.creation.appError", + "translation": "Nepodařilo se vytvořit dočasný CSV soubor." + }, + { + "id": "ent.compliance.csv.attachment.export.appError", + "translation": "Nepodařilo se přidat přílohu do CSV exportu." + }, + { + "id": "ent.compliance.csv.attachment.copy.appError", + "translation": "Nepodařilo se skopírovat přílohu do zip souboru." + }, + { + "id": "ent.compliance.bad_export_type.appError", + "translation": "Neznámý formát {{.ExportType}}" + }, + { + "id": "ent.cluster.save_config.error", + "translation": "Konzola systému je v režimu read-only pokud je zapnuté High Availability a v konfiguraci je aktivovaná volba ReadOnlyConfig." + }, + { + "id": "ent.cluster.config_changed.info", + "translation": "Konfigurace clusteru id={{.id}} se změnila. Cluster může být nestabilní a může vyžadovat restart. Pro zajišťění správného chování by ste měli ihned provést rolling restart." + }, + { + "id": "ent.account_migration.get_saml_users_failed", + "translation": "Nepodařilo se získat SAML uživatele." + }, + { + "id": "ent.account_migration.get_all_failed", + "translation": "Nepodařilo se získat uživatele." + }, + { + "id": "brand.save_brand_image.save_image.app_error", + "translation": "Nepodařilo se zapsat obrázek do souborového úložiště. Prosím zkontrolujte svoje síťové spojení a zkuste to znovu." + }, + { + "id": "brand.save_brand_image.open.app_error", + "translation": "Nepodařilo se nahrát vlastní logo. Zkontrolujte, že obrázek je menší než 2 MB a zkuste to znovu." + }, + { + "id": "brand.save_brand_image.encode.app_error", + "translation": "Nepodařilo se zkonvertovat obrázek do formátu PNG. Prosím zkuste to znovu." + }, + { + "id": "brand.save_brand_image.decode.app_error", + "translation": "Nepodařilo se dekódovat obrázek." + }, + { + "id": "bleveengine.search_users_in_team.error", + "translation": "Vyhledávání uživatele se nezdařilo." + }, + { + "id": "bleveengine.search_users_in_channel.uchan.error", + "translation": "Vyhledávání uživatele se nezdařilo." + }, + { + "id": "bleveengine.search_users_in_channel.nuchan.error", + "translation": "Vyhledávání uživatele se nezdařilo." + }, + { + "id": "bleveengine.search_posts.error", + "translation": "Vyhledávání se nezdařilo." + }, + { + "id": "bleveengine.search_channels.error", + "translation": "Vyhledávání se nezdařilo." + }, + { + "id": "bleveengine.indexer.index_batch.nothing_left_to_index.error", + "translation": "Zkouším vytvořit index pro novou dávku, když jsou všechny entity dokončené." + }, + { + "id": "bleveengine.indexer.do_job.parse_start_time.error", + "translation": "Indexovacímu procesu Elasticsearch se nepodařilo rozluštit čas začátku." + }, + { + "id": "bleveengine.indexer.do_job.parse_end_time.error", + "translation": "Indexovacímu procesu Elasticsearch se nepodařilo rozluštit čas konce." + }, + { + "id": "bleveengine.index_user.error", + "translation": "Nepodařilo se zaindexovat uživatele." + }, + { + "id": "bleveengine.index_post.error", + "translation": "Nepodařilo se zaindexovat příspěvek." + }, + { + "id": "bleveengine.index_channel.error", + "translation": "Nepodařilo se zaindexovat kanál." + }, + { + "id": "bleveengine.delete_user.error", + "translation": "Nepodařilo se smazat uživatele." + }, + { + "id": "bleveengine.delete_post.error", + "translation": "Chyba při mazání příspěvku." + }, + { + "id": "bleveengine.delete_channel.error", + "translation": "Nepodařilo se smazat kanál." + }, + { + "id": "app.user_access_token.invalid_or_missing", + "translation": "Neplatný nebo chybějící token." + }, + { + "id": "app.user_access_token.disabled", + "translation": "Podpora pro osobní přístupové tokeny je na tomto serveru vypnutá. Prosím kontaktujte svého administrátora." + }, + { + "id": "app.team.rename_team.name_occupied", + "translation": "Nelze přejmenovat tým, jméno je již použito." + }, + { + "id": "app.team.join_user_to_team.max_accounts.app_error", + "translation": "Tento tým dosáhl maximálního počtu povolených účtu. Kontaktujte prosím systémového administrátora a nechte nastavit vyšší limit." + }, + { + "id": "app.team.invite_token.group_constrained.error", + "translation": "Nelze se připojit k týmu s omezeným přístupem na základě tokenu." + }, + { + "id": "app.team.invite_id.group_constrained.error", + "translation": "Nelze se připojit ke skupině s omezeným přístupem na základě pozvánky." + }, + { + "id": "app.system_install_date.parse_int.app_error", + "translation": "Nepodařilo se zpracovat datum instalace." + }, + { + "id": "app.submit_interactive_dialog.json_error", + "translation": "Vyskytla se chyba kódování JSON pro interaktivní dialog." + }, + { + "id": "app.schemes.is_phase_2_migration_completed.not_completed.app_error", + "translation": "Tento API endpoint není dostupný protože potřebné migrace ještě neskončili." + }, + { + "id": "app.save_config.app_error", + "translation": "Při ukládání konfigurace došlo k chybě." + }, + { + "id": "app.role.check_roles_exist.role_not_found", + "translation": "Role neexistuje" + }, + { + "id": "app.plugin.write_file.saving.app_error", + "translation": "Při ukládání souboru došlo k chybě." + }, + { + "id": "app.plugin.write_file.read.app_error", + "translation": "Při čtení souboru došlo k chybě." + }, + { + "id": "app.plugin.webapp_bundle.app_error", + "translation": "Nelze vytvořit zásuvný modul balíčku webové aplikace." + }, + { + "id": "app.plugin.upload_disabled.app_error", + "translation": "Podpora pro zásuvné moduly je vypnutá." + }, + { + "id": "app.plugin.sync.read_local_folder.app_error", + "translation": "Chyba čtení ze složky místních zásuvných modulů." + }, + { + "id": "app.plugin.sync.list_filestore.app_error", + "translation": "Chyba čtení souboru z uložiště zásuvných modulů." + }, + { + "id": "app.plugin.store_signature.app_error", + "translation": "Nepodařilo se uložit podpis zásuvného modulu do nastaveného souborového úložiště." + }, + { + "id": "app.plugin.store_bundle.app_error", + "translation": "Nelze uložit zásuvný modul do nastaveného úložiště." + }, + { + "id": "app.plugin.signature_decode.app_error", + "translation": "Nepodařilo se rozluštit podpis v base64 formátu." + }, + { + "id": "app.plugin.restart.app_error", + "translation": "Po aktualizaci se nepodařilo restartovat zásuvný modul." + }, + { + "id": "app.plugin.remove_bundle.app_error", + "translation": "Nelze odebrat balíček zásuvného modulu z úložiště." + }, + { + "id": "app.plugin.remove.app_error", + "translation": "Nelze vymazat zásuvný modul." + }, + { + "id": "app.plugin.not_installed.app_error", + "translation": "Zásuvný modul není nainstalován." + }, + { + "id": "app.plugin.mvdir.app_error", + "translation": "Nepodařilo se přesunout zásuvný modul z dočasného do cílového adresáře. Zdá se, že stejný adresář už používá jiný modul." + }, + { + "id": "app.plugin.modify_saml.app_error", + "translation": "SAML soubory nelze upravovat." + }, + { + "id": "app.plugin.marshal.app_error", + "translation": "Nepodařilo se připravit zásuvné moduly tržiště." + }, + { + "id": "app.plugin.marketplace_plugins.signature_not_found.app_error", + "translation": "Nepodařilo se nalézt podpis požadovaného zásuvného modulu z tržiště." + }, + { + "id": "app.plugin.marketplace_plugins.not_found.app_error", + "translation": "Nepodařilo se nalézt požadovaný zásuvný modul na tržišti." + }, + { + "id": "app.plugin.marketplace_plugin_request.app_error", + "translation": "Nepodařilo se rozluštit požadavek na tržiště zásuvných modulů." + }, + { + "id": "app.plugin.marketplace_disabled.app_error", + "translation": "Tržiště je vypnuté. Podrobnosti najdete v logu." + }, + { + "id": "app.plugin.marketplace_client.failed_to_fetch", + "translation": "Nepodařilo se získat zásuvné moduly ze serveru marketplace." + }, + { + "id": "app.plugin.marketplace_client.app_error", + "translation": "Nelze vytvořit klienta pro marketplace." + }, + { + "id": "app.plugin.manifest.app_error", + "translation": "Nepodařilo se najít manifest zásuvného modulu." + }, + { + "id": "app.plugin.invalid_version.app_error", + "translation": "Nepodařilo se načíst verzi zásuvného modulu." + }, + { + "id": "app.plugin.invalid_id.app_error", + "translation": "ID zásuvného modulu musí mít délku minimálně {{.Min}} a maximálně {{.Max}} znaků a vyhovovat regulárnímu výrazu {{.Regex}}." + }, + { + "id": "app.plugin.install_marketplace_plugin.app_error", + "translation": "Nepodařilo se nainstalovat zásuvný modul z marketplace." + }, + { + "id": "app.plugin.install_id_failed_remove.app_error", + "translation": "Nepodařilo se nainstalovat zásuvný modul. Modul se stejným ID je již nainstalován a nepodařilo se ho odstranit." + }, + { + "id": "app.plugin.install_id.app_error", + "translation": "Nepodařilo se nainstalovat zásuvný modul. Modul se stejným ID je již nainstalován." + }, + { + "id": "app.plugin.install.app_error", + "translation": "Nepodařilo se nainstalovat zásuvný modul." + }, + { + "id": "app.plugin.get_statuses.app_error", + "translation": "Nepodařilo se získat stavy zásuvných modulů." + }, + { + "id": "app.plugin.get_public_key.get_file.app_error", + "translation": "Došlo k chybě při získávání veřejného klíče z úložiště." + }, + { + "id": "store.sql_team.save_member.exists.app_error", + "translation": "Člen týmu s daným ID již existuje." + }, + { + "id": "app.scheme.save.app_error", + "translation": "Nepodařilo se založit schéma." + }, + { + "id": "app.scheme.get.app_error", + "translation": "Nepodařilo se získat schéma." + }, + { + "id": "app.reaction.save.save.app_error", + "translation": "NElze uložit reakci." + }, + { + "id": "app.reaction.delete_all_with_emoji_name.get_reactions.app_error", + "translation": "Nelze odstranit reakce s daným jménem emotikonu." + }, + { + "id": "store.sql_post.search.disabled", + "translation": "Hledání bylo zakázáno na tomto serveru. Obraťte se na správce systému." + }, + { + "id": "app.emoji.delete.no_results", + "translation": "Nelze najít emoji určené ke smazání." + }, + { + "id": "app.emoji.delete.app_error", + "translation": "Nelze smazat emoji." + }, + { + "id": "app.plugin.get_plugins.app_error", + "translation": "Nelze získat aktivní zásuvné moduly." + }, + { + "id": "app.plugin.get_cluster_plugin_statuses.app_error", + "translation": "Nepodařilo se získat stavy zásuvných modulů z clusteru." + }, + { + "id": "app.plugin.flag_managed.app_error", + "translation": "Nelze nastavit zásuvný modul jako spravovaný úložištěm souborů." + }, + { + "id": "app.plugin.filesystem.app_error", + "translation": "Došlo k chybě souborového systému." + }, + { + "id": "app.plugin.extract.app_error", + "translation": "Při rozbalování balíčku se zásuvným modulem došlo k chybě." + }, + { + "id": "app.plugin.disabled.app_error", + "translation": "Zásuvné moduly jsou vypnuté. Podrobnosti najdete v logu." + }, + { + "id": "app.plugin.delete_public_key.delete.app_error", + "translation": "Došlo k chybě při mazání veřejného klíče." + }, + { + "id": "app.plugin.deactivate.app_error", + "translation": "Nelze deaktivovat zásuvný modul." + }, + { + "id": "app.plugin.config.app_error", + "translation": "Chyba při ukládání stavu zásuvného modulu v konfiguraci." + }, + { + "id": "app.plugin.cluster.save_config.app_error", + "translation": "Konfigurace zásuvných modulů ve vašem souboru config.json musí být upravena ručně pokud používáte ReadOnlyConfig s povoleným clustrováním." + }, + { + "id": "app.notification.subject.notification.full", + "translation": "[{{.SiteName}}] Nové upozornění v {{ .TeamName}} z {{.Day}}.{{.Month}}.{{.Year}}" + }, + { + "id": "app.notification.subject.group_message.generic", + "translation": "[{{.SiteName}}] Nová skupinová zpráva z {{.Day}}.{{.Month}}.{{.Year}}" + }, + { + "id": "app.notification.subject.group_message.full", + "translation": "[{{.SiteName}}] Nová skupinová zpráva v {{ .ChannelName}} z {{.Day}}.{{.Month}}.{{.Year}}" + }, + { + "id": "app.notification.subject.direct.full", + "translation": "[{{.SiteName}}] Nové přímé zprávy od {{.SenderDisplayName}} z {{.Day}}.{{.Month}}.{{.Year}}" + }, + { + "id": "app.import.validate_user_teams_import_data.team_name_missing.error", + "translation": "V členství uživatele chybí název týmu." + }, + { + "id": "app.import.validate_user_teams_import_data.invalid_team_theme.error", + "translation": "Neplatný motiv vzhledu týmu pro uživatele" + }, + { + "id": "app.import.validate_user_teams_import_data.invalid_roles.error", + "translation": "Neplatné role uživatele pro členství v týmech." + }, + { + "id": "app.import.validate_user_import_data.username_missing.error", + "translation": "Chybějící povinná vlastnost uživatele: uživatelské jméno." + }, + { + "id": "app.import.validate_user_import_data.username_invalid.error", + "translation": "Uživatelské jméno není platné." + }, + { + "id": "app.import.validate_user_import_data.roles_invalid.error", + "translation": "Uživatelské role nejsou platné." + }, + { + "id": "app.import.validate_user_import_data.profile_image.error", + "translation": "Neplatný profilový obrázek." + }, + { + "id": "app.import.validate_user_import_data.position_length.error", + "translation": "Poloha uživatele je příliš dlouhá." + }, + { + "id": "app.import.validate_user_import_data.password_length.error", + "translation": "Heslo uživatele nemá platnou délku." + }, + { + "id": "app.import.validate_user_import_data.notify_props_mobile_push_status_invalid.error", + "translation": "Neplatná hodnota Mobile Push Status Notify Prop pro uživatele." + }, + { + "id": "app.import.validate_user_import_data.notify_props_mobile_invalid.error", + "translation": "Neplatná hodnota Mobile Notify Prop pro uživatele." + }, + { + "id": "app.import.validate_user_import_data.notify_props_email_invalid.error", + "translation": "Neplatná hodnota Email Notify Prop pro uživatele." + }, + { + "id": "app.import.validate_user_import_data.notify_props_desktop_sound_invalid.error", + "translation": "Neplatná hodnota Desktop Sound Notify Prop pro uživatele." + }, + { + "id": "app.import.validate_user_import_data.notify_props_desktop_invalid.error", + "translation": "Neplatná hodnota Desktop Notify Prop pro uživatele." + }, + { + "id": "app.import.validate_user_import_data.notify_props_comments_trigger_invalid.error", + "translation": "Neplatná hodnota Comments Prop pro uživatele." + }, + { + "id": "app.import.validate_user_import_data.notify_props_channel_trigger_invalid.error", + "translation": "Neplatná hodnota Channel Trigger Notify Prop pro uživatele." + }, + { + "id": "app.import.validate_user_import_data.nickname_length.error", + "translation": "Přezdívka uživatele je příliš dlouhá." + }, + { + "id": "app.import.validate_user_import_data.last_name_length.error", + "translation": "Příjmení uživatele je příliš dlouhé." + }, + { + "id": "app.import.validate_user_import_data.first_name_length.error", + "translation": "Jméno uživatele je příliš dlouhé." + }, + { + "id": "app.import.validate_user_import_data.email_missing.error", + "translation": "Chybí povinná vlastnost uživatele: email." + }, + { + "id": "app.import.validate_user_import_data.email_length.error", + "translation": "Emailová adresa uživatele má neplatnou délku." + }, + { + "id": "app.import.validate_user_import_data.auth_data_length.error", + "translation": "Položka uživatele AuthData je příliš dlouhá." + }, + { + "id": "app.import.validate_user_import_data.auth_data_and_password.error", + "translation": "Položky uživatele AuthData a Password nemohou být použity zároveň." + }, + { + "id": "app.import.validate_user_import_data.advanced_props_show_unread_section.error", + "translation": "Neplatné nastavení nepřečtené sekce pro uživatele" + }, + { + "id": "app.import.validate_user_import_data.advanced_props_formatting.error", + "translation": "Neplatné nastavení formátování příspěvků uživatele" + }, + { + "id": "app.import.validate_user_import_data.advanced_props_feature_markdown_preview.error", + "translation": "Neplatné nastavení sníženého náhledu pro uživatele" + }, + { + "id": "app.import.validate_user_import_data.advanced_props_email_interval.error", + "translation": "Neplatné nastavení intervalu dávky e-mailu pro uživatele" + }, + { + "id": "app.import.validate_user_channels_import_data.invalid_roles.error", + "translation": "Neplatná role v záznamech členství uživatele." + }, + { + "id": "app.import.validate_user_channels_import_data.invalid_notify_props_mobile.error", + "translation": "Neplatná vlastnost Mobile upozornění v záznamech členství uživatele." + }, + { + "id": "app.import.validate_user_channels_import_data.invalid_notify_props_mark_unread.error", + "translation": "Neplatná vlastnost MarkUnread upozornění v záznamech členství uživatele." + }, + { + "id": "app.import.validate_user_channels_import_data.invalid_notify_props_desktop.error", + "translation": "Neplatná vlastnost Desktop upozornění v záznamech členství uživatele." + }, + { + "id": "app.import.validate_user_channels_import_data.channel_name_missing.error", + "translation": "V záznamech členství uživatele chybí název kanálu." + }, + { + "id": "app.import.validate_team_import_data.type_missing.error", + "translation": "Chybí požadovaná vlastnost týmu: type." + }, + { + "id": "app.import.validate_team_import_data.type_invalid.error", + "translation": "Typ týmu není platný." + }, + { + "id": "app.import.validate_team_import_data.scheme_invalid.error", + "translation": "Neplatný název schématu pro tým." + }, + { + "id": "app.import.validate_team_import_data.name_reserved.error", + "translation": "Jméno kanálu obsahuje vyhrazená slova." + }, + { + "id": "app.import.validate_team_import_data.name_missing.error", + "translation": "Chybějící požadovaná vlastnost týmu: jméno." + }, + { + "id": "app.import.validate_team_import_data.name_length.error", + "translation": "Jméno týmu je příliš dlouhé." + }, + { + "id": "app.import.validate_team_import_data.name_characters.error", + "translation": "Jméno kanálu obsahuje neplatné znaky." + }, + { + "id": "app.import.validate_team_import_data.display_name_missing.error", + "translation": "Chybí požadovaná vlastnost týmu: display_name." + }, + { + "id": "app.import.validate_team_import_data.display_name_length.error", + "translation": "Tým display_name nesplňuje povolenou délku omezení." + }, + { + "id": "app.import.validate_team_import_data.description_length.error", + "translation": "Popis týmu je příliš dlouhý." + }, + { + "id": "app.import.validate_scheme_import_data.wrong_roles_for_scope.error", + "translation": "Schématu v tomto rámci byly přiděleny špatné role." + }, + { + "id": "app.import.validate_scheme_import_data.unknown_scheme.error", + "translation": "Neznámý rámec (scope) schématu." + }, + { + "id": "app.import.validate_scheme_import_data.null_scope.error", + "translation": "Rámec (scope) schématu je povinný." + }, + { + "id": "app.import.validate_scheme_import_data.name_invalid.error", + "translation": "Neplatné jméno schématu." + }, + { + "id": "app.import.validate_scheme_import_data.display_name_invalid.error", + "translation": "Neplatné zobrazované jméno schématu." + }, + { + "id": "app.import.validate_scheme_import_data.description_invalid.error", + "translation": "Neplatný popis schématu." + }, + { + "id": "app.import.validate_role_import_data.name_invalid.error", + "translation": "Neplatné jméno role." + }, + { + "id": "app.import.validate_role_import_data.invalid_permission.error", + "translation": "Neplatné oprávnění role." + }, + { + "id": "app.import.validate_role_import_data.display_name_invalid.error", + "translation": "Neplatné zobrazované jméno role." + }, + { + "id": "app.import.validate_role_import_data.description_invalid.error", + "translation": "Neplatný popis role." + }, + { + "id": "app.import.validate_reply_import_data.user_missing.error", + "translation": "Chybějící požadovaná vlastnost odpovědi: Uživatel." + }, + { + "id": "app.import.validate_reply_import_data.message_missing.error", + "translation": "Chybějící požadovaná položka odpovědi: Message." + }, + { + "id": "app.import.validate_reply_import_data.message_length.error", + "translation": "Položka Message v odpovědi na zprávu je příliš dlouhá." + }, + { + "id": "app.import.validate_reply_import_data.create_at_zero.error", + "translation": "Vlastnost CreateAt v odpovědi nesmí být nula." + }, + { + "id": "app.import.validate_reply_import_data.create_at_missing.error", + "translation": "Chybí požadovaná vlastnost odpovědi: create_at." + }, + { + "id": "app.import.validate_reaction_import_data.user_missing.error", + "translation": "Chybí požadovaná vlastnost odpovědi: Uživatel." + }, + { + "id": "app.import.validate_reaction_import_data.emoji_name_missing.error", + "translation": "Chybí požadovaná vlastnost odpovědi: EmojiName." + }, + { + "id": "app.import.validate_reaction_import_data.emoji_name_length.error", + "translation": "Položka EmojiName v odpovědi je delší než je maximální povolená délka." + }, + { + "id": "app.import.validate_reaction_import_data.create_at_zero.error", + "translation": "Položka CreateAt odpovědi nesmí být nula." + }, + { + "id": "app.import.validate_reaction_import_data.create_at_missing.error", + "translation": "Chybějící požadovaná vlastnost odpovědi: create_at." + }, + { + "id": "app.import.validate_reaction_import_data.create_at_before_parent.error", + "translation": "Položka CreateAt v odpovědi musí být větší než CreateAt u nadřazeného příspěvku." + }, + { + "id": "app.import.validate_post_import_data.user_missing.error", + "translation": "Chybí požadovaná vlastnost příspěvku: Uživatel." + }, + { + "id": "app.import.validate_channel_import_data.display_name_length.error", + "translation": "Kanál display_name nesplňuje povolenou délku." + }, + { + "id": "app.import.process_import_data_file_version_line.invalid_version.error", + "translation": "Nelze načíst verzi souboru importovaných dat." + }, + { + "id": "app.import.import_user_teams.save_preferences.error", + "translation": "Nastavení týmového motivu nelze uložit" + }, + { + "id": "app.import.import_user_channels.save_preferences.error", + "translation": "Chyba při importu členství v kanálech uživatelů. Nepodařilo se uložit předvolby." + }, + { + "id": "app.import.import_user.save_preferences.error", + "translation": "Při importu uživatelských předvoleb došlo k chybě. Nepodařilo se uložit předvolby." + }, + { + "id": "app.import.import_team.scheme_wrong_scope.error", + "translation": "Tým musí být přidělen do schématu v rámci týmu." + }, + { + "id": "app.import.import_team.scheme_deleted.error", + "translation": "Nelze nastavit tým, aby používal smazané schéma." + }, + { + "id": "app.import.import_scheme.scope_change.error", + "translation": "Hromadný import nemůže změnit rozsah již existujícího režimu." + }, + { + "id": "app.import.import_post.user_not_found.error", + "translation": "Při importu příspěvku došlo k chybě. Uživatel s uživatelským jménem \"{{.Username}}\" nebyl nalezen." + }, + { + "id": "app.import.import_post.save_preferences.error", + "translation": "Při importu příspěvku došlo k chybě. Nepodařilo se uložit předvolby." + }, + { + "id": "app.import.import_post.channel_not_found.error", + "translation": "Chyba při importu příspěvku. Kanál s názvem \"{{.ChannelName}}\" nebyl nalezen." + }, + { + "id": "app.import.import_line.unknown_line_type.error", + "translation": "Import datového řádku má neznámý typ \"{{.Type}}\"." + }, + { + "id": "app.import.import_line.null_user.error", + "translation": "Import datového řádku má typ \"user\", ale objekt user je null." + }, + { + "id": "app.import.import_line.null_team.error", + "translation": "Import datového řádku má typ \"team\", ale objekt team je null." + }, + { + "id": "app.import.import_line.null_scheme.error", + "translation": "Import datového řádku má typ \"scheme\", ale objekt scheme je null." + }, + { + "id": "app.import.import_line.null_post.error", + "translation": "Import datového řádku má typ \"post\", ale objekt post je null." + }, + { + "id": "app.import.import_line.null_emoji.error", + "translation": "Import datového řádku má typ \"emoji\", ale objekt emoji je null." + }, + { + "id": "app.import.import_line.null_direct_post.error", + "translation": "Import datového řádku má typ \"direct_post\", ale objekt direct_post je null." + }, + { + "id": "app.import.import_line.null_direct_channel.error", + "translation": "Import datového řádku má typ \"direct_channel\", ale direct_channel je null." + }, + { + "id": "app.import.import_line.null_channel.error", + "translation": "Import datového řádku má typ \"kanál\", ale objekt kanálu je null." + }, + { + "id": "app.import.import_direct_post.create_group_channel.error", + "translation": "Nepodařilo se získat skupinový kanál" + }, + { + "id": "app.import.import_direct_post.create_direct_channel.error", + "translation": "Nepodařilo se získat přímý kanál" + }, + { + "id": "app.import.import_direct_channel.update_header_failed.error", + "translation": "Nepodařilo se aktualizovat záhlaví přímého kanálu" + }, + { + "id": "app.import.import_direct_channel.create_group_channel.error", + "translation": "Vytvoření skupinového kanálu selhalo" + }, + { + "id": "app.import.import_direct_channel.create_direct_channel.error", + "translation": "Vytvoření přímého kanálu selhalo" + }, + { + "id": "app.import.import_channel.team_not_found.error", + "translation": "Chyba při importování kanálu. Tým s názvem \"{{.TeamName}}\" nebyl nalezen." + }, + { + "id": "app.import.import_channel.scheme_wrong_scope.error", + "translation": "Kanál musí být přiřazen ke schématu v rámci kanálu." + }, + { + "id": "app.import.import_channel.scheme_deleted.error", + "translation": "Nelze nastavit kanál s použitím smazaného schématu." + }, + { + "id": "app.import.emoji.bad_file.error", + "translation": "Chyba čtení importovaného souboru s obrázky emoji. Emoji se jménem: \"{{.EmojiName}}\"" + }, + { + "id": "app.import.bulk_import.unsupported_version.error", + "translation": "Nesprávná nebo chybějící verze v souboru importu dat. Ujistěte se, že verze je prvním objektem v importovaném souboru a zkuste to znovu." + }, + { + "id": "app.import.bulk_import.json_decode.error", + "translation": "Dekódování řádku JSON selhalo." + }, + { + "id": "app.import.bulk_import.file_scan.error", + "translation": "Chyba při načítání souboru s daty pro import." + }, + { + "id": "app.import.attachment.file_upload.error", + "translation": "Chyba při nahrávání souboru: \"{{.FilePath}}\"" + }, + { + "id": "app.import.attachment.bad_file.error", + "translation": "Chyba při čtení souboru: \"{{.FilePath}}\"" + }, + { + "id": "app.export.export_write_line.json_marshall.error", + "translation": "Došlo k chybě při shomažďování JSON dat pro export." + }, + { + "id": "app.export.export_write_line.io_writer.error", + "translation": "Došlo k chybě při zápisu exportních dat." + }, + { + "id": "app.export.export_custom_emoji.copy_emoji_images.error", + "translation": "Nelze zkopírovat uživatelské emoji obrázky" + }, + { + "id": "app.channel.post_update_channel_purpose_message.updated_to", + "translation": "%s změnil záhlaví kanálu na: %s" + }, + { + "id": "app.channel.post_update_channel_purpose_message.updated_from", + "translation": "%s změnil záhlaví kanálu z: %s na: %s" + }, + { + "id": "app.channel.post_update_channel_purpose_message.retrieve_user.error", + "translation": "Nepodařilo se načíst uživatele při aktualizaci účelu kanálu %v" + }, + { + "id": "app.channel.post_update_channel_purpose_message.removed", + "translation": "%s odstranil záhlaví kanálu (was: %s)" + }, + { + "id": "app.channel.post_update_channel_purpose_message.post.error", + "translation": "Nepodařilo se uložit popis cíle kanálu" + }, + { + "id": "app.channel.move_channel.members_do_not_match.error", + "translation": "Nelze přesunout kanál, pokud všichni jeho členové již nejsou členy cílového týmu." + }, + { + "id": "app.channel.create_direct_channel.internal_error", + "translation": "Nepodařilo se aktualizovat kanál." + }, + { + "id": "app.channel.create_channel.no_team_id.app_error", + "translation": "Musíte zadat ID týmu pro vytvoření kanálu." + }, + { + "id": "app.channel.create_channel.internal_error", + "translation": "Nepodařilo se uložit kanál." + }, + { + "id": "app.bot.permenent_delete.bad_id", + "translation": "Chyba při mazání účtu bota." + }, + { + "id": "app.bot.patchbot.internal_error", + "translation": "Nepodařilo se aktualizovat bota." + }, + { + "id": "app.bot.getbots.internal_error", + "translation": "Nepodařilo se získat seznam botů." + }, + { + "id": "app.bot.getbot.internal_error", + "translation": "Nepodařilo se získat bota." + }, + { + "id": "app.bot.get_disable_bot_sysadmin_message", + "translation": "{{if .disableBotsSetting}}{{if .printAllBots}}Uživatel {{.UserName}} byl zneplatněn. Uživatel spravoval následující účty robotů, které byly nyní též zneplatněny.\n\n{{.BotNames}}{{else}}Uživatel {{.UserName}} byl zneplatněn. Uživatel spravoval určitý počet ({{.NumBots}}) účty robotů, které byly nyní též zneplatněny. Jsou to také tyto:\n\n{{.BotNames}}{{end}}Můžeš převzít vlastnictví každého robota tím, že to povolíš v nabídce **Integrace > Účty robotů** a vytvoříš nového peška pro robota.\n\nPro více informací, viz naše [dokumentace](https://docs.mattermost.com/developer/bot-accounts.html#what-happens-when-a-user-who-owns-bot-accounts-is-disabled).{{else}}{{if .printAllBots}}Uživatel {{.UserName}} byl zneplatněn. Uživatel spravoval následující účty robotů, které jsou stále zplatněny.\n\n{{.BotNames}}\n{{else}}Uživatel {{.UserName}} byl zneplatněn. Uživatel spravoval určitý počet ({{.NumBots}}) účty robotů, , které jsou stále zplatněny. Jsou to také tyto:\n\n{{.BotNames}}{{end}}Silně doporučujeme převzít vlastiství nad těmito roboty tím, že je povolíš v nabídce **Integrace > Účty robotů** a vytvoříš nového peška pro robota.\n\nPro více informací, viz naše [dokumentace](https://docs.mattermost.com/developer/bot-accounts.html#what-happens-when-a-user-who-owns-bot-accounts-is-disabled).\n\nPokud chceš, aby se účty robotů automaticky zneplatniIy po zneplatnění uživatele, nastav na hodnotu \"pravda\" položku \"Zneplatnit účty robotů po zneplatnění uživatele\" v nabídce **Konzola Systému > Integrace > Účty robotů**.{{end}}" + }, + { + "id": "app.bot.createbot.internal_error", + "translation": "Nepodařilo se uložit bota." + }, + { + "id": "app.admin.test_site_url.failure", + "translation": "Toto není platná URL adresa" + }, + { + "id": "app.admin.test_email.failure", + "translation": "Neúspěšné připojení: {{.Error}}" + }, + { + "id": "app.admin.saml.invalid_response_from_idp.app_error", + "translation": "Nepodařilo se přečíst odpověď obdrženou od poskytovatele identit." + }, + { + "id": "app.admin.saml.failure_read_response_body_from_idp.app_error", + "translation": "Při čtení obsahu odpovědi od poskytovatele identit došlo k selhání." + }, + { + "id": "app.admin.saml.failure_decode_metadata_xml_from_idp.app_error", + "translation": "Nedaří se dekódovat XML metadata obdržená od poskytovatele identity." + }, + { + "id": "api.websocket_handler.server_busy.app_error", + "translation": "Server je zaneprázdněn, nekritické služby jsou dočasně nedostupné." + }, + { + "id": "api.websocket_handler.invalid_param.app_error", + "translation": "Nevalidní {{.Name}} parametr." + }, + { + "id": "api.webhook.update_outgoing.intersect.app_error", + "translation": "Odchozí webhooky ze stejného kanálu nemohou mít stejné spouštěcí slova / adresy URL zpětného volání." + }, + { + "id": "api.webhook.team_mismatch.app_error", + "translation": "Nepovedlo se aktualizovat webhooky napříč teamy." + }, + { + "id": "api.webhook.incoming.error", + "translation": "Nelze dekódovat multipart payload příchozího webhooku." + }, + { + "id": "api.webhook.create_outgoing.triggers.app_error", + "translation": "Musí být nastaven buď trigger_words nebo channel_id." + }, + { + "id": "api.webhook.create_outgoing.permissions.app_error", + "translation": "Neplatná oprávnění k vytvoření odchozího webhooku." + }, + { + "id": "api.webhook.create_outgoing.not_open.app_error", + "translation": "Odchozí webhooky lze nastavit jen pro veřejné kanály." + }, + { + "id": "api.webhook.create_outgoing.intersect.app_error", + "translation": "Odchozí webhooky ze stejného kanálu nemohou mít stejné spouštěcí slova / adresy URL zpětného volání." + }, + { + "id": "api.web_socket_router.not_authenticated.app_error", + "translation": "Připojení pro websocket není ověřeno. Přihlaste se a zkuste to znovu." + }, + { + "id": "api.web_socket_router.no_action.app_error", + "translation": "Žádná akce websocket." + }, + { + "id": "api.web_socket_router.bad_seq.app_error", + "translation": "Neplatná sekvence zprávy pro websocket." + }, + { + "id": "api.web_socket_router.bad_action.app_error", + "translation": "Neznámá akce websocketu." + }, + { + "id": "api.web_socket.connect.upgrade.app_error", + "translation": "Nelze aktualizovat připojení přes websocket." + }, + { + "id": "api.user.verify_email.token_parse.error", + "translation": "Nepodařilo se spracovat token z emailové verifikace" + }, + { + "id": "api.user.verify_email.link_expired.app_error", + "translation": "Odkaz pro emailovou verifikaci expiroval." + }, + { + "id": "api.user.verify_email.broken_token.app_error", + "translation": "Neúspěšná verifikace tokenu z e-mailu." + }, + { + "id": "api.user.verify_email.bad_link.app_error", + "translation": "Špatný ověřovací odkaz." + }, + { + "id": "api.user.upload_profile_user.upload_profile.app_error", + "translation": "Nelze nahrát profilový obrázek." + }, + { + "id": "api.user.upload_profile_user.too_large.app_error", + "translation": "Nepodařilo se nahrát profilový obrázek. Soubor je příliš velký." + }, + { + "id": "api.user.upload_profile_user.storage.app_error", + "translation": "Nelze nahrát soubor. Místo pro ukládání souborů není nakonfigurované." + }, + { + "id": "api.user.upload_profile_user.parse.app_error", + "translation": "Nelze zpracovat vícedílné formuláře." + }, + { + "id": "api.user.upload_profile_user.open.app_error", + "translation": "Soubor obrázku se nepodařilo otevřít." + }, + { + "id": "api.user.upload_profile_user.no_file.app_error", + "translation": "Žádný soubor pod \"image\" v žádosti." + }, + { + "id": "api.user.upload_profile_user.encode.app_error", + "translation": "Nepodařilo se zakódovat profilový obrázek." + }, + { + "id": "api.user.upload_profile_user.decode.app_error", + "translation": "Nepodařilo se dekódovat profilový obrázek." + }, + { + "id": "api.user.upload_profile_user.array.app_error", + "translation": "Prázdné pole pod \"image\" v žádosti." + }, + { + "id": "api.user.update_user.accepted_guest_domain.app_error", + "translation": "Zadaný e-mail nepatří do zvolené domény. Obraťte se prosím na správce nebo zaregistrujte jiný e-mail." + }, + { + "id": "api.user.update_user.accepted_domain.app_error", + "translation": "Zadaný e-mail nepatří do zvolené domény. Obraťte se prosím na správce nebo zaregistrujte jiný e-mail." + }, + { + "id": "api.user.update_password.valid_account.app_error", + "translation": "Aktualizační heslo se nezdařilo, protože příslušný platný účet nebyl nalezen." + }, + { + "id": "api.user.update_password.oauth.app_error", + "translation": "Aktualizovat heslo se nezdařilo, protože uživatel je přihlášen prostřednictvím služby OAuth." + }, + { + "id": "api.user.update_password.menu", + "translation": "pomocí nabídky nastavení" + }, + { + "id": "api.user.update_password.incorrect.app_error", + "translation": "Aktuálně zadané heslo je nesprávné. Prosím, zkontrolujte, zda je vypnutý Caps Lock a zkuste to znovu." + }, + { + "id": "api.user.update_password.failed.app_error", + "translation": "Aktualizace hesla selhala." + }, + { + "id": "api.user.update_password.context.app_error", + "translation": "Změna hesla není možná, protože hodnota user_id se neshoduje s user_id." + }, + { + "id": "api.user.update_oauth_user_attrs.get_user.app_error", + "translation": "Nelze získat uživatele z uživatelského objektu {{.Service}}." + }, + { + "id": "api.user.update_active.permissions.app_error", + "translation": "Nemáte potřebná oprávnění." + }, + { + "id": "api.user.update_active.not_enable.app_error", + "translation": "Nemůžete se sami deaktivovat, protože tato funkce není povolena. Obraťte se na správce systému." + }, + { + "id": "api.user.update_active.cannot_enable_guest_when_guest_feature_is_disabled.app_error", + "translation": "Nelze aktivovat účet hosta, protože funkce \"Přístup Hosta\" není povolena." + }, + { + "id": "api.user.send_verify_email_and_forget.failed.error", + "translation": "Nelze odeslat e-mail s verifikací" + }, + { + "id": "api.user.send_sign_in_change_email_and_forget.error", + "translation": "Nelze odeslat e-mail o změně hesla" + }, + { + "id": "api.user.send_password_reset.sso.app_error", + "translation": "Heslo k účtům SSO nelze obnovit." + }, + { + "id": "api.user.send_password_reset.send.app_error", + "translation": "Odeslání e-mailu s žádostí o změnu hesla se nezdařilo." + }, + { + "id": "api.user.send_email_change_verify_email_and_forget.error", + "translation": "Nelze odeslat email o změně verifikačního e-mailu" + }, + { + "id": "api.user.send_deactivate_email_and_forget.failed.error", + "translation": "Nelze odeslat email o deaktivaci účtu" + }, + { + "id": "api.user.saml.not_available.app_error", + "translation": "SAML 2.0 není nakonfigurován nebo není podporován tímto serverem." + }, + { + "id": "api.user.reset_password.token_parse.error", + "translation": "Nelze analyzovat token reset hesla" + }, + { + "id": "api.user.reset_password.sso.app_error", + "translation": "Heslo k účtům SSO nelze obnovit." + }, + { + "id": "api.user.reset_password.method", + "translation": "použití odkazu pro obnovení hesla" + }, + { + "id": "api.user.reset_password.link_expired.app_error", + "translation": "Platnost odkazu pro obnovení hesla vypršela." + }, + { + "id": "api.user.reset_password.invalid_link.app_error", + "translation": "Odkaz na obnovení hesla není platný." + }, + { + "id": "api.user.reset_password.broken_token.app_error", + "translation": "Token pro obnovení hesla není platný." + }, + { + "id": "api.user.promote_guest_to_user.no_guest.app_error", + "translation": "Hosta nelze převést na běžného uživatele, protože už není host." + }, + { + "id": "api.user.oauth_to_email.not_available.app_error", + "translation": "Autentizační přenos není na tomto serveru dostupný nebo nakonfigurovaný." + }, + { + "id": "api.user.oauth_to_email.context.app_error", + "translation": "Změna hesla není možná, protože hodnota user_id se neshoduje se zadaným ID uživatele." + }, + { + "id": "api.user.login_ldap.not_available.app_error", + "translation": "Funkce AD/LDAP je na tomto serveru nedostupná." + }, + { + "id": "api.user.login_by_oauth.parse.app_error", + "translation": "Nelze zpracovat auth data z uživatelského objektu {{.Service}}." + }, + { + "id": "api.user.login_by_oauth.not_available.app_error", + "translation": "{{.Service}} SSO prostřednictvím OAuth 2.0 není na tomto serveru k dispozici." + }, + { + "id": "model.config.is_valid.ldap_id", + "translation": "Pole AD/LDAP \"ID Attribute\" je povinné." + }, + { + "id": "model.config.is_valid.ldap_email", + "translation": "AD/LDAP pole \"Email Attribute\" je povinné." + }, + { + "id": "model.config.is_valid.ldap_basedn", + "translation": "AD/LDAP pole \"BaseDN\" je povinné." + }, + { + "id": "model.config.is_valid.image_proxy_type.app_error", + "translation": "Neplatný typ proxy. Musí být 'local' nebo 'atmos/camo'." + }, + { + "id": "model.config.is_valid.group_unread_channels.app_error", + "translation": "Invalid group unread channels for service settings. Musí být 'disabled', 'default_on', nebo 'default_off'." + }, + { + "id": "model.config.is_valid.file_salt.app_error", + "translation": "Nevalidní veřejný link pro nastavení souboru. Musí mít 32 znaků nebo více." + }, + { + "id": "model.config.is_valid.file_driver.app_error", + "translation": "Neplatný název ovladače. Musí být 'local' nebo 'amazons3'." + }, + { + "id": "model.config.is_valid.encrypt_sql.app_error", + "translation": "Neplatný šifrovací klíč pro nastavení SQL. Musí být 32 znaků nebo více." + }, + { + "id": "model.config.is_valid.email_security.app_error", + "translation": "Neplatné zabezpečení připojení pro nastavení e-mailu. Musí být 'TLS' nebo 'STARTTLS'." + }, + { + "id": "model.config.is_valid.email_notification_contents_type.app_error", + "translation": "Neplatný typ obsahu e-mailového oznámení pro nastavení e-mailu. Musí to být buď „full“ nebo „generic“." + }, + { + "id": "model.config.is_valid.email_batching_buffer_size.app_error", + "translation": "Neplatné nastavení velikost vyrovnávací paměti pro e-mail dávky. Musí být nulové nebo kladné číslo." + }, + { + "id": "model.config.is_valid.elastic_search.request_timeout_seconds.app_error", + "translation": "Elasticsearch Request Timeout musí být minimálně 1 vteřina." + }, + { + "id": "model.config.is_valid.elastic_search.posts_aggregator_job_start_time.app_error", + "translation": "Elasticsearch PostsAggregatorJobStartTime musí být časový údaj ve formátu \"hh:mm\"." + }, + { + "id": "model.config.is_valid.elastic_search.live_indexing_batch_size.app_error", + "translation": "Elasticsearch Live Indexing Batch Size musí být nastaveno minimálně na 1." + }, + { + "id": "model.config.is_valid.elastic_search.enable_searching.app_error", + "translation": "Nastavení indexace pro Elasticsearch musí být povoleno pokud hodnota Elasticsearch EnableSearching je také povolena" + }, + { + "id": "model.config.is_valid.elastic_search.enable_autocomplete.app_error", + "translation": "Nastavení indexace pro Elasticsearch musí být povoleno pokud hodnota Elasticsearch EnableAutocomplete je také povolena" + }, + { + "id": "model.config.is_valid.elastic_search.connection_url.app_error", + "translation": "Elasticsearch ConnectionUrl nastavení musí být zadáno když je Elastic Search index zapnutý." + }, + { + "id": "model.config.is_valid.elastic_search.aggregate_posts_after_days.app_error", + "translation": "Elasticsearch AggregatePostsAfterDays musí být číslo větší nebo rovno 1." + }, + { + "id": "model.config.is_valid.display.custom_url_schemes.app_error", + "translation": "Vlastní schéma URL {{.Scheme}} je neplatné. Schémata vlastních URL musí začínat písmenem a obsahovat pouze písmena, čísla, plus (+), tečku (.) a spojovník (-)." + }, + { + "id": "model.config.is_valid.data_retention.message_retention_days_too_low.app_error", + "translation": "Zprávy je nutno uchovávat minimálně jeden den." + }, + { + "id": "model.config.is_valid.data_retention.file_retention_days_too_low.app_error", + "translation": "Soubor je nutno uchovávat minimálně jeden den." + }, + { + "id": "model.config.is_valid.data_retention.deletion_job_start_time.app_error", + "translation": "Čas zahájení úlohy uchovávání dat musí být v 24hodinovém formátu ve tvaru HH:MM." + }, + { + "id": "model.config.is_valid.cluster_email_batching.app_error", + "translation": "Při aktivovaném clusteringu nelze aktivovat dávkování e-mailu." + }, + { + "id": "model.config.is_valid.bleve_search.enable_searching.app_error", + "translation": "Elasticsearch IndexingEnabled musí být nastaven na \"true\" když je Elastic Search SearchEnabled nastaven na \"true\"" + }, + { + "id": "model.config.is_valid.bleve_search.enable_autocomplete.app_error", + "translation": "Nastavení indexace pro Bleve musí být povoleno pokud hodnota Bleve EnableAutocomplete je také povolena" + }, + { + "id": "model.config.is_valid.atmos_camo_image_proxy_url.app_error", + "translation": "Neplatné RemoteImageProxyURL proatmos/camo. Musí být nastaven na sdílený klíč." + }, + { + "id": "model.config.is_valid.atmos_camo_image_proxy_options.app_error", + "translation": "Neplatné RemoteImageProxyOptions pro atmos/camo. Musí být nastaven na sdílený klíč." + }, + { + "id": "model.config.is_valid.allow_cookies_for_subdomains.app_error", + "translation": "Povolení cookies pro subdomény vyžaduje nastavení SiteURL." + }, + { + "id": "model.compliance.is_valid.start_end_at.app_error", + "translation": "Do musí být větší než Od." + }, + { + "id": "model.compliance.is_valid.start_at.app_error", + "translation": "Od musí být platný časový údaj." + }, + { + "id": "model.compliance.is_valid.id.app_error", + "translation": "Nevalidní Id." + }, + { + "id": "model.compliance.is_valid.end_at.app_error", + "translation": "Do musí být platný časový údaj." + }, + { + "id": "model.compliance.is_valid.desc.app_error", + "translation": "Neplatný popis." + }, + { + "id": "model.compliance.is_valid.create_at.app_error", + "translation": "Čas vytvoření musí být platný časový údaj." + }, + { + "id": "model.command_hook.user_id.app_error", + "translation": "Neplatné uživatelské id." + }, + { + "id": "model.command_hook.root_id.app_error", + "translation": "Neplatné ID roota." + }, + { + "id": "model.command_hook.id.app_error", + "translation": "Neplatný command hook ID." + }, + { + "id": "model.command_hook.create_at.app_error", + "translation": "Čas vytvoření musí být platný časový údaj." + }, + { + "id": "model.command_hook.command_id.app_error", + "translation": "Neplatné ID příkazu." + }, + { + "id": "model.command_hook.channel_id.app_error", + "translation": "Neplatné id kanálu." + }, + { + "id": "model.command.is_valid.user_id.app_error", + "translation": "Neplatné uživatelské id." + }, + { + "id": "model.command.is_valid.url_http.app_error", + "translation": "Neplatné URL. Musí to být platná adresa a začínat http:// nebo https://." + }, + { + "id": "model.command.is_valid.url.app_error", + "translation": "Neplatné URL." + }, + { + "id": "model.command.is_valid.update_at.app_error", + "translation": "Čas aktualizace musí být platný časový údaj." + }, + { + "id": "model.command.is_valid.trigger.app_error", + "translation": "Chybný spouštěč." + }, + { + "id": "model.command.is_valid.token.app_error", + "translation": "Neplatný token." + }, + { + "id": "model.command.is_valid.team_id.app_error", + "translation": "Neplatné ID týmu." + }, + { + "id": "model.command.is_valid.method.app_error", + "translation": "Neplatná metoda." + }, + { + "id": "model.command.is_valid.id.app_error", + "translation": "Nevalidní Id." + }, + { + "id": "model.command.is_valid.display_name.app_error", + "translation": "Neplatný název." + }, + { + "id": "model.command.is_valid.description.app_error", + "translation": "Neplatný popis." + }, + { + "id": "model.command.is_valid.create_at.app_error", + "translation": "Čas vytvoření musí být platný časový údaj." + }, + { + "id": "model.cluster.is_valid.type.app_error", + "translation": "Type musí být nastaveno." + }, + { + "id": "model.cluster.is_valid.name.app_error", + "translation": "ClusterName musí být nastaveno." + }, + { + "id": "model.cluster.is_valid.last_ping_at.app_error", + "translation": "LastPingAt musí být nastaveno." + }, + { + "id": "model.cluster.is_valid.id.app_error", + "translation": "Nevalidní Id." + }, + { + "id": "model.cluster.is_valid.hostname.app_error", + "translation": "Hostname musí být nastaveno." + }, + { + "id": "model.cluster.is_valid.create_at.app_error", + "translation": "CreateAt musí být nastaveno." + }, + { + "id": "model.channel_member.is_valid.user_id.app_error", + "translation": "Neplatné uživatelské id." + }, + { + "id": "model.channel_member.is_valid.unread_level.app_error", + "translation": "Neplatná úroveň pro označení nepřečtených." + }, + { + "id": "model.channel_member.is_valid.push_level.app_error", + "translation": "Neplatná úroveň notifikací." + }, + { + "id": "model.channel_member.is_valid.notify_level.app_error", + "translation": "Neplatná úroveň upozornění." + }, + { + "id": "model.channel_member.is_valid.ignore_channel_mentions_value.app_error", + "translation": "Neplatný stav ignorování zmínek v kanálu." + }, + { + "id": "model.channel_member.is_valid.email_value.app_error", + "translation": "Neplatná hodnota e-mailu." + }, + { + "id": "model.channel_member.is_valid.channel_id.app_error", + "translation": "Neplatné id kanálu." + }, + { + "id": "model.channel.is_valid.update_at.app_error", + "translation": "Čas aktualizace musí být platný časový údaj." + }, + { + "id": "model.channel.is_valid.type.app_error", + "translation": "Neplatný typ." + }, + { + "id": "model.channel.is_valid.purpose.app_error", + "translation": "Neplatný účel." + }, + { + "id": "model.channel.is_valid.name.app_error", + "translation": "Neplatný název kanálu, nepoužívejte uživatelská jména v názvech kanálů, které neslouží k přímé komunikaci." + }, + { + "id": "model.channel.is_valid.id.app_error", + "translation": "Nevalidní Id." + }, + { + "id": "model.channel.is_valid.header.app_error", + "translation": "Neplatná hlavička." + }, + { + "id": "model.channel.is_valid.display_name.app_error", + "translation": "Neplatné zobrazované jméno." + }, + { + "id": "model.channel.is_valid.creator_id.app_error", + "translation": "Neplatné ID autora." + }, + { + "id": "model.channel.is_valid.create_at.app_error", + "translation": "Čas vytvoření musí být platný časový údaj." + }, + { + "id": "model.bot.is_valid.username.app_error", + "translation": "Neplatné uživatelské jméno." + }, + { + "id": "model.bot.is_valid.user_id.app_error", + "translation": "Neplatné uživatelské id." + }, + { + "id": "model.bot.is_valid.update_at.app_error", + "translation": "Neplatné datum aktualizace." + }, + { + "id": "model.bot.is_valid.description.app_error", + "translation": "Neplatný popis." + }, + { + "id": "model.bot.is_valid.creator_id.app_error", + "translation": "Neplatné ID autora." + }, + { + "id": "model.bot.is_valid.create_at.app_error", + "translation": "Neplatné create at." + }, + { + "id": "model.authorize.is_valid.user_id.app_error", + "translation": "Neplatné uživatelské id." + }, + { + "id": "model.authorize.is_valid.state.app_error", + "translation": "Neplatný stav." + }, + { + "id": "model.authorize.is_valid.scope.app_error", + "translation": "Neplatný rozsah." + }, + { + "id": "model.authorize.is_valid.response_type.app_error", + "translation": "Neplatný typ odpovědi." + }, + { + "id": "model.authorize.is_valid.redirect_uri.app_error", + "translation": "Neplatné URL přesměrování." + }, + { + "id": "model.authorize.is_valid.expires.app_error", + "translation": "Expirace musí být nastavena." + }, + { + "id": "model.authorize.is_valid.create_at.app_error", + "translation": "Čas vytvoření musí být platný časový údaj." + }, + { + "id": "model.authorize.is_valid.client_id.app_error", + "translation": "Neplatné ID klienta." + }, + { + "id": "model.authorize.is_valid.auth_code.app_error", + "translation": "Neplatný autorizační kód." + }, + { + "id": "model.access.is_valid.user_id.app_error", + "translation": "Neplatné uživatelské id." + }, + { + "id": "model.access.is_valid.refresh_token.app_error", + "translation": "Neplatný token obnovení." + }, + { + "id": "model.access.is_valid.redirect_uri.app_error", + "translation": "Neplatné URL přesměrování." + }, + { + "id": "model.access.is_valid.client_id.app_error", + "translation": "Neplatné ID klienta." + }, + { + "id": "model.access.is_valid.access_token.app_error", + "translation": "Neplatný přístupový token." + }, + { + "id": "migrations.worker.run_migration.unknown_key", + "translation": "Nelze spustit migrační úlohu z důvodu neznámého migračního klíče." + }, + { + "id": "migrations.worker.run_advanced_permissions_phase_2_migration.invalid_progress", + "translation": "Přenos selhal z důvodu chybných dat k průběhu." + }, + { + "id": "mfa.validate_token.authenticate.app_error", + "translation": "Neplatný MFA token." + }, + { + "id": "mfa.mfa_disabled.app_error", + "translation": "Vícefaktorová autentizace byla na tomto zakázána." + }, + { + "id": "mfa.generate_qr_code.create_code.app_error", + "translation": "Chyba při generování QR kódu." + }, + { + "id": "mfa.activate.bad_token.app_error", + "translation": "Neplatný MFA token." + }, + { + "id": "mattermost.bulletin.subject", + "translation": "Bezpečnostní zpravodaj Mattermostu" + }, + { + "id": "manaultesting.test_autolink.unable.app_error", + "translation": "Nepodařilo se načíst kanály." + }, + { + "id": "manaultesting.manual_test.parse.app_error", + "translation": "Nelze zpracovat URL." + }, + { + "id": "jobs.set_job_error.update.error", + "translation": "Nepodařilo se nastavit úlohu do stavu chyba" + }, + { + "id": "jobs.request_cancellation.status.error", + "translation": "Nelze vyžádat zrušení úlohy, která není ve stavu, který lze zrušit." + }, + { + "id": "interactive_message.generate_trigger_id.signing_failed", + "translation": "Nepodařilo se podepsat generované ID triggeru pro interaktivní dialog." + }, + { + "id": "interactive_message.decode_trigger_id.verify_signature_failed", + "translation": "Ověření podpisu selhalo ID triggeru pro interaktivní dialog." + }, + { + "id": "interactive_message.decode_trigger_id.signature_decode_failed", + "translation": "Nepodařilo se dekódovat base64 podpis ID triggeru pro interaktivní dialog." + }, + { + "id": "interactive_message.decode_trigger_id.missing_data", + "translation": "Trigger ID chybí požadovaná data pro interaktivní dialog." + }, + { + "id": "interactive_message.decode_trigger_id.expired", + "translation": "Trigger ID pro interaktivní dialog vypršel. ID triggeru existuje maximálně po dobu {{.Seconds}} vteřin." + }, + { + "id": "interactive_message.decode_trigger_id.base64_decode_failed_signature", + "translation": "NNepodařilo se dekódovat base64 podpis ID interaktivní dialogu." + }, + { + "id": "interactive_message.decode_trigger_id.base64_decode_failed", + "translation": "Nepodařilo se dekódovat base64 pro ID triggeru pro interaktivní dialog." + }, + { + "id": "groups.unsupported_syncable_type", + "translation": "Nepodporované syncable typu '{{.Value}}'." + }, + { + "id": "ent.saml.service_disable.app_error", + "translation": "SAML 2.0 není nakonfigurován nebo není podporován tímto serverem." + }, + { + "id": "ent.saml.metadata.app_error", + "translation": "Nastala chyba při sestavování metadat služby." + }, + { + "id": "ent.saml.license_disable.app_error", + "translation": "Vaše licence nepodporuje SAML ověřování." + }, + { + "id": "ent.saml.do_login.validate.app_error", + "translation": "Při ověřování odpovědi od Identity Providera došlo k chybě. Obraťte se na správce systému." + }, + { + "id": "ent.saml.do_login.parse.app_error", + "translation": "Při analýze odpovědi poskytovatele identit došlo k chybě. Obraťte se na správce systému." + }, + { + "id": "ent.saml.do_login.empty_response.app_error", + "translation": "Od Identity Providera jsme obdrželi prázdnou odpověď." + }, + { + "id": "ent.saml.do_login.decrypt.app_error", + "translation": "SAML přihlášení nebylo úspěšné, protože při dešifrování odpovědi od Identity Providera došlo k chybě. Prosím obraťte se na správce systému." + }, + { + "id": "ent.saml.configure.not_encrypted_response.app_error", + "translation": "Přihlášení SAML bylo neúspěšné, protože odpověď poskytovatele totožnosti není šifrována. Obraťte se na správce systému." + }, + { + "id": "ent.saml.configure.load_private_key.app_error", + "translation": "Přihlášení SAML bylo neúspěšné, protože veřejný klíč Service Providera nebyl nalezen. Obraťte se na správce systému." + }, + { + "id": "ent.saml.configure.load_idp_cert.app_error", + "translation": "Soubor s veřejný certifikátem Identity Providera nebyl nalezen. Obraťte se na správce systému." + }, + { + "id": "ent.saml.configure.encryption_not_enabled.app_error", + "translation": "Přihlášení SAML bylo neúspěšné, protože šifrování není povoleno. Obraťte se na správce systému." + }, + { + "id": "ent.saml.build_request.encoding.app_error", + "translation": "Při kódování požadavku na poskytovatele totožnosti došlo k chybě. Obraťte se na správce systému." + }, + { + "id": "ent.saml.build_request.app_error", + "translation": "Při iniciování žádosti na Identity Providera došlo k chybě.. Obraťte se na správce systému." + }, + { + "id": "ent.saml.attribute.app_error", + "translation": "Přihlášení SAML bylo neúspěšné, protože jeden z atributů je nesprávný. Obraťte se na správce systému." + }, + { + "id": "ent.migration.migratetosaml.username_already_used_by_other_user", + "translation": "Uživatelské jméno již použivá jiný uživatel Mattermost." + }, + { + "id": "ent.migration.migratetosaml.user_not_found_in_users_mapping_file", + "translation": "Uživatel nebyl v souboru uživatelů nalezen." + }, + { + "id": "ent.migration.migratetosaml.email_already_used_by_other_user", + "translation": "E-mail již použil jiný SAML uživatel." + }, + { + "id": "ent.migration.migratetoldap.user_not_found", + "translation": "Nepodařilo se najít uživatele na AD/LDAP serveru: " + }, + { + "id": "ent.migration.migratetoldap.duplicate_field", + "translation": "Nelze migrovat uživatele AD / LDAP se zadaným polem. Byl zjištěn duplicitní záznam. Odeberte prosím všechny duplikáty a zkuste to znovu." + }, + { + "id": "ent.message_export.global_relay_export.deliver.unable_to_open_zip_file_data.app_error", + "translation": "Nelze otevřít dočasný soubor exportu." + }, + { + "id": "ent.message_export.global_relay_export.deliver.unable_to_open_email_file.app_error", + "translation": "Nelze získat e-mail z dočasného souboru." + }, + { + "id": "ent.message_export.global_relay_export.deliver.unable_to_get_file_info.app_error", + "translation": "Nezdařilo se získat informace o dočasném souboru pro export." + }, + { + "id": "ent.message_export.global_relay_export.deliver.to_address.app_error", + "translation": "Nepodařilo se nastavit emailovou adresu To:." + }, + { + "id": "ent.message_export.global_relay_export.deliver.parse_mail.app_error", + "translation": "Nepodařilo se pročíst informace emailu." + }, + { + "id": "ent.message_export.global_relay_export.deliver.msg_data.app_error", + "translation": "Nepodařilo se zapsat emailovou zprávu." + }, + { + "id": "ent.message_export.global_relay_export.deliver.msg.app_error", + "translation": "Nepodařilo se nastavit emailovou zprávu." + }, + { + "id": "ent.message_export.global_relay_export.deliver.from_address.app_error", + "translation": "Nepodařilo se nastavit emailovou adresu From:." + }, + { + "id": "ent.message_export.global_relay_export.deliver.close.app_error", + "translation": "Nepodařilo se doručit email na Global Relay." + }, + { + "id": "ent.message_export.global_relay.generate_email.app_error", + "translation": "Nepodařilo se vygenerovat eml data." + }, + { + "id": "ent.message_export.global_relay.create_file_in_zip.app_error", + "translation": "Nepodařilo se vytvořit eml soubor." + }, + { + "id": "ent.message_export.global_relay.close_zip_file.app_error", + "translation": "Nepodařilo se zavřít zip soubor." + }, + { + "id": "ent.message_export.global_relay.attach_file.app_error", + "translation": "Nepodařilo se přidat přílohu do Global Relay exportu." + }, + { + "id": "ent.ldap_groups.reachable_groups_error", + "translation": "Chyba při načítání skupin uživatele" + }, + { + "id": "ent.ldap_groups.no_rows", + "translation": "nebyly nalezeny žádné skupiny s odpovídajícím uid" + }, + { + "id": "ent.ldap_groups.members_of_group_error", + "translation": "při načítání členů skupiny došlo k chybě" + }, + { + "id": "ent.ldap_groups.groups_search_error", + "translation": "chyba při načítání ldap skupin" + }, + { + "id": "ent.ldap_groups.group_search_error", + "translation": "chyba při načítání ldap skupiny" + }, + { + "id": "ent.ldap.validate_guest_filter.app_error", + "translation": "Neplatný AD/LDAP filter na návštěvníky." + }, + { + "id": "ent.ldap.validate_filter.app_error", + "translation": "Neplatný AD/LDAP filter." + }, + { + "id": "ent.ldap.validate_admin_filter.app_error", + "translation": "Neplatný AD/LDAP filtr na správce." + }, + { + "id": "ent.ldap.syncronize.search_failure_size_exceeded.app_error", + "translation": "Překročen limit. Zkuste zkontrolovat [max page size](https://docs.mattermost.com/deployment/sso-ldap.html#i-see-the-log-error-ldap-result-code-4-size-limit-exceeded)." + }, + { + "id": "ent.ldap.syncronize.search_failure.app_error", + "translation": "Nepodařilo se vyhletat uživatele na AD/LDAP serveru. Otestujte zda se Mattermost dokáže připojit k AD/LDAP serveru a zkuste to znova." + }, + { + "id": "ent.ldap.syncronize.populate_syncables", + "translation": "chyba při naplňování synchronizací" + }, + { + "id": "ent.ldap.syncronize.get_all_groups.app_error", + "translation": "chyba při načítání ldap skupin." + }, + { + "id": "ent.ldap.syncronize.get_all.app_error", + "translation": "Nelze získat všechny uživatele pomocí AD/LDAP." + }, + { + "id": "ent.ldap.syncronize.delete_group_constained_memberships", + "translation": "chyba výmazu příslušnosti k týmu nebo kanálu" + }, + { + "id": "ent.ldap.do_login.user_not_registered.app_error", + "translation": "Uživatel není registrován na AD/LDAP serveru." + }, + { + "id": "ent.ldap.do_login.user_filtered.app_error", + "translation": "Váš účet AD / LDAP nemá oprávnění používat tento Mattermost server. Požádejte správce systému, aby zkontroloval uživatelský filtr AD/LDAP." + }, + { + "id": "ent.ldap.do_login.unable_to_connect.app_error", + "translation": "Nelze se spojit se serverem AD/LDAP." + }, + { + "id": "ent.ldap.do_login.search_ldap_server.app_error", + "translation": "Nepodařilo se prohledat AD/LDAP server." + }, + { + "id": "ent.ldap.do_login.matched_to_many_users.app_error", + "translation": "Dané uživatelské jméno má vícero uživatelů." + }, + { + "id": "ent.ldap.do_login.licence_disable.app_error", + "translation": "Funkce AD/LDAP je zakázána aktuální licencí. Obraťte se na správce systému o inovaci podnikové licence." + }, + { + "id": "ent.ldap.do_login.invalid_password.app_error", + "translation": "Neplatné heslo." + }, + { + "id": "ent.ldap.do_login.bind_admin_user.app_error", + "translation": "Nepodařilo se připojit k AD/LDAP serveru. Zkontrolujte BindUsername a BindPassword." + }, + { + "id": "ent.ldap.disabled.app_error", + "translation": "Služba AD / LDAP zakázaná nebo licence nepodporuje službu AD/LDAP." + }, + { + "id": "ent.ldap.create_fail", + "translation": "Nelze vytvořit LDAP uživatele." + }, + { + "id": "ent.ldap.app_error", + "translation": "ldap interface byl prázdný." + }, + { + "id": "ent.id_loaded.license_disable.app_error", + "translation": "Vaše licence nepodporuje push upozornění založené na ID." + }, + { + "id": "ent.elasticsearch.test_config.reenter_password", + "translation": "URL nemo uživatelské jméno Elasticsearch serveru se změnilo. Zadejte prosím znovu heslo." + }, + { + "id": "ent.elasticsearch.test_config.license.error", + "translation": "Vaše licence nepodporuje Elasticsearch." + }, + { + "id": "ent.elasticsearch.test_config.indexing_disabled.error", + "translation": "Elasticsearch je vypnutý." + }, + { + "id": "ent.elasticsearch.stop.already_stopped.app_error", + "translation": "Elasticsearch je už zastaven." + }, + { + "id": "ent.elasticsearch.start.start_bulk_processor_failed.app_error", + "translation": "Nepodařilo se spustit Elasticsearch dávkový procesor." + }, + { + "id": "ent.elasticsearch.start.parse_server_version.app_error", + "translation": "Nepodařilo se zpracovat verzi Elasticsearch serveru." + }, + { + "id": "ent.elasticsearch.start.get_server_version.app_error", + "translation": "Nepodařilo se získat verzi Elasticsearch serveru." + }, + { + "id": "api.user.login_by_oauth.bot_login_forbidden.app_error", + "translation": "Přihlášení bota je zakázáno." + }, + { + "id": "api.user.login.use_auth_service.app_error", + "translation": "Přihlaste se prosím pomocí {{.AuthService}}." + }, + { + "id": "api.user.login.not_verified.app_error", + "translation": "Přihlášení se nezdařilo, protože e-mailová adresa dosud nebyla ověřena." + }, + { + "id": "api.user.login.invalid_credentials_username", + "translation": "Zadejte platné uživatelské jméno a/nebo heslo." + }, + { + "id": "api.user.login.invalid_credentials_sso", + "translation": "Zadejte správný email nebo uživatelské jméno a/nebo heslo, nebo se přihlašte jiným způsobem." + }, + { + "id": "api.user.login.invalid_credentials_email_username", + "translation": "Zadejte platný e-mail, uživatelské jméno nebo heslo." + }, + { + "id": "api.user.login.invalid_credentials_email", + "translation": "Zadejte platný e-mail nebo heslo" + }, + { + "id": "api.user.login.inactive.app_error", + "translation": "Přihlášení se nezdařilo, protože váš účet byl deaktivován. Kontaktujte správce." + }, + { + "id": "api.user.login.guest_accounts.license.error", + "translation": "Vaše licence nepodporuje účty typu host" + }, + { + "id": "api.user.login.guest_accounts.disabled.error", + "translation": "Účty typu host jsou vypnuté" + }, + { + "id": "api.user.login.client_side_cert.license.app_error", + "translation": "Pokus o použití experimentální funkce ClientSideCertEnable bez platné licence Enterprise." + }, + { + "id": "api.user.login.client_side_cert.certificate.app_error", + "translation": "Pokusili jste se přihlásit pomocí experimentální funkce ClientSideCert bez poskytnutí platného certifikátu." + }, + { + "id": "api.user.login.bot_login_forbidden.app_error", + "translation": "Přihlášení robota je zakázáno." + }, + { + "id": "api.user.login.blank_pwd.app_error", + "translation": "Pole s heslem nemůže být prázdné" + }, + { + "id": "api.user.ldap_to_email.not_ldap_account.app_error", + "translation": "Tento uživatelský účet nepoužívá AD/LDAP." + }, + { + "id": "api.user.ldap_to_email.not_available.app_error", + "translation": "Funkce AD/LDAP je na tomto serveru nedostupná." + }, + { + "id": "api.user.get_user_by_email.permissions.app_error", + "translation": "Nelze najít uživatele podle emailu." + }, + { + "id": "api.user.email_to_oauth.not_available.app_error", + "translation": "Autentizační transfer není na tomto serveru dostupný nebo nakonfigurovaný." + }, + { + "id": "api.user.email_to_ldap.not_available.app_error", + "translation": "Funkce AD/LDAP je na tomto serveru nedostupná." + }, + { + "id": "api.user.demote_user_to_guest.already_guest.app_error", + "translation": "Uživatele se nepodařilo změnit na hosta, protože už hostem je." + }, + { + "id": "api.user.create_user.signup_link_invalid.app_error", + "translation": "Tento registrační odkaz není platný." + }, + { + "id": "api.user.create_user.signup_link_expired.app_error", + "translation": "Platnost registračního odkazu vypršela." + }, + { + "id": "api.user.create_user.signup_email_disabled.app_error", + "translation": "Registrace e-mailem není povolena." + }, + { + "id": "api.user.create_user.no_open_server", + "translation": "Tento server neumožňuje otevřené registrace. Chcete-li dostat pozvání, obraťte se na správce." + }, + { + "id": "api.user.create_user.invalid_invitation_type.app_error", + "translation": "Nelze vytvořit uživatele, neplatná pozvánka." + }, + { + "id": "api.user.create_user.guest_accounts.license.app_error", + "translation": "Vaše licence nepodporuje účty typu host." + }, + { + "id": "api.user.create_user.guest_accounts.disabled.app_error", + "translation": "Účty typu Host jsou vypnuty." + }, + { + "id": "api.user.create_user.disabled.app_error", + "translation": "Vytváření uživatelů je vypnuto." + }, + { + "id": "api.user.create_user.accepted_domain.app_error", + "translation": "Zadaný e-mail nepatří do zvolené domény. Obraťte se prosím na správce nebo zaregistrujte jiný e-mail." + }, + { + "id": "api.user.create_profile_image.initial.app_error", + "translation": "Do výchozího profilového obrázku se nepodařilo přidat iniciály uživatele." + }, + { + "id": "api.user.create_profile_image.encode.app_error", + "translation": "Nepodařilo se dekódovat profilový obrázek." + }, + { + "id": "api.user.create_profile_image.default_font.app_error", + "translation": "Nelze vytvořit výchozí font obrázku profilu." + }, + { + "id": "api.user.create_password_token.error", + "translation": "Nelze vytvořit token pro obnovení hesla" + }, + { + "id": "api.user.create_oauth_user.create.app_error", + "translation": "Nelze vytvořit uživatele dle objektu uživatele {{.Service}}." + }, + { + "id": "api.user.create_oauth_user.already_attached.app_error", + "translation": "K této e-mailové adrese je již přiřazen účet, který používá jinou metodu než {{.Service}}. Přihlaste se prosím pomocí {{.Auth}}." + }, + { + "id": "api.user.create_email_token.error", + "translation": "Nepodařilo se vytvořit token pro ověření e-mailu" + }, + { + "id": "api.user.complete_switch_with_oauth.parse.app_error", + "translation": "Nelze zpracovat auth data z uživatelského objektu {{.Service}}." + }, + { + "id": "api.user.complete_switch_with_oauth.blank_email.app_error", + "translation": "Prázdný email." + }, + { + "id": "api.user.check_user_password.invalid.app_error", + "translation": "Přihlášení se nezdařilo z důvodu neplatného hesla." + }, + { + "id": "api.user.check_user_mfa.bad_code.app_error", + "translation": "Neplatný MFA token." + }, + { + "id": "api.user.check_user_login_attempts.too_many.app_error", + "translation": "Váš účet je uzamčen z důvodu příliš mnoha neúspěšných pokusech o zadání hesla. Prosím, resetujte své heslo." + }, + { + "id": "api.user.authorize_oauth_user.unsupported.app_error", + "translation": "{{.Service}} SSO skrze OAuth 2.0 není dostupné na tomto serveru." + }, + { + "id": "api.user.authorize_oauth_user.token_failed.app_error", + "translation": "Požadavek tokenu selhal." + }, + { + "id": "api.user.authorize_oauth_user.service.app_error", + "translation": "Požadavek tokenu na službu {{.Service}} se nezdařil." + }, + { + "id": "api.user.authorize_oauth_user.response.app_error", + "translation": "Od poskytovatele služby OAuth přišla neplatná odpověď." + }, + { + "id": "api.user.authorize_oauth_user.missing.app_error", + "translation": "Chybějící přístupový token." + }, + { + "id": "api.user.authorize_oauth_user.invalid_state.app_error", + "translation": "Neplatný stav" + }, + { + "id": "api.user.authorize_oauth_user.bad_token.app_error", + "translation": "Špatný typ tokenu." + }, + { + "id": "api.user.authorize_oauth_user.bad_response.app_error", + "translation": "Špatná odpověď na žádost o token." + }, + { + "id": "api.user.add_direct_channels_and_forget.failed.error", + "translation": "Nepodařilo se přidat předvolby přímých kanálů pro uživatele user_id={{.UserId}}, team_id={{.TeamId}}, err={{.Error}}" + }, + { + "id": "api.user.activate_mfa.email_and_ldap_only.app_error", + "translation": "Služba MFA není k dispozici pro tento typ účtu." + }, + { + "id": "api.templates.welcome_subject", + "translation": "[{{ .SiteName }}] Připojili jste se k {{ .ServerURL }}" + }, + { + "id": "api.templates.welcome_body.title", + "translation": "Připojili jste se k {{ .ServerURL }}" + }, + { + "id": "api.templates.welcome_body.info", + "translation": "Ověřte prosím svou e-mailovou adresu níže." + }, + { + "id": "api.templates.welcome_body.button", + "translation": "Ověřit e-mail" + }, + { + "id": "api.templates.welcome_body.app_download_info", + "translation": "Pro nejlepší zážitek si stáhněte aplikace pro PC, Mac, iOS a Android." + }, + { + "id": "api.templates.verify_subject", + "translation": "[{{ .SiteName }}] Ověření Emailu" + }, + { + "id": "api.templates.verify_body.title", + "translation": "Jste připojeni k {{ .ServerURL }}" + }, + { + "id": "api.templates.verify_body.info", + "translation": "Ověřte prosím svou e-mailovou adresu kliknutím níže." + }, + { + "id": "api.templates.verify_body.button", + "translation": "Ověření emailu" + }, + { + "id": "api.templates.username_change_subject", + "translation": "[{{.SiteName}}] Vaše uživatelské jméno se změnilo" + }, + { + "id": "api.templates.username_change_body.title", + "translation": "Aktualizovali jste uživatelské jméno" + }, + { + "id": "api.templates.username_change_body.info", + "translation": "Vaše uživatelské jméno pro {{.TeamDisplayName}} bylo změněno na {{.NewUsername}}." + }, + { + "id": "api.templates.user_access_token_subject", + "translation": "[{{.SiteName}}] Osobní přístupový token byl přidán do vašeho účtu" + }, + { + "id": "api.templates.user_access_token_body.title", + "translation": "Osobní přístupový token byl přidán do vašeho účtu" + }, + { + "id": "api.templates.user_access_token_body.info", + "translation": "Do účtu {{.SiteURL}} byl do účtu přidán osobní přístupový token. Mohou být použity pro přístup k účtu {{.SiteName}}." + }, + { + "id": "api.templates.signin_change_email.subject", + "translation": "[{{.SiteName}}] Vaše přihlašovací metoda byla aktualizována" + }, + { + "id": "api.templates.signin_change_email.body.title", + "translation": "Aktualizoval jste svou metodu přihlášení" + }, + { + "id": "api.templates.signin_change_email.body.method_email", + "translation": "email a heslo" + }, + { + "id": "api.templates.signin_change_email.body.info", + "translation": "Aktualizoval jste svou metodu přihlášení na {{.SiteName}} na {{.Method}}." + }, + { + "id": "api.templates.reset_subject", + "translation": "[{{ .SiteName }}] Resetujte své heslo" + }, + { + "id": "api.templates.reset_body.title", + "translation": "Bylo požádáno o nastavení nového hesla" + }, + { + "id": "api.templates.reset_body.button", + "translation": "Obnovit heslo" + }, + { + "id": "api.templates.post_body.button", + "translation": "Přejít na příspěvek" + }, + { + "id": "api.templates.password_change_subject", + "translation": "[{{.SiteName}}] Vaše heslo bylo aktualizováno" + }, + { + "id": "api.templates.password_change_body.title", + "translation": "Vaše heslo bylo změněno" + }, + { + "id": "api.templates.password_change_body.info", + "translation": "Vaše heslo pro {{.TeamDisplayName}} bylo aktualizováno na {{.TeamURL}} do {{.Method}}." + }, + { + "id": "api.templates.mfa_deactivated_body.title", + "translation": "Autentizace více faktory byla odstraněna" + }, + { + "id": "api.templates.mfa_deactivated_body.info", + "translation": "Víceúrovňové přihlášení bylo odstraněno z vašeho účtu na {{ .SiteURL }}." + }, + { + "id": "api.templates.mfa_change_subject", + "translation": "[{{.SiteName}}] Vaše MFA bylo aktualizováno" + }, + { + "id": "api.templates.mfa_activated_body.title", + "translation": "Vícefázové ověření bylo přidáno" + }, + { + "id": "api.templates.mfa_activated_body.info", + "translation": "Do účtu {{.SiteURL}} bylo přidáno vícestupňové ověřování." + }, + { + "id": "api.templates.invite_subject", + "translation": "[{{.SiteName}}] {{.SenderName}} vás zve do skupiny {{.TeamDisplayName}}" + }, + { + "id": "api.templates.invite_guest_subject", + "translation": "[{{.SiteName}}] {{.SenderName}} vás zve do skupiny {{.TeamDisplayName}} jako hosta" + }, + { + "id": "api.templates.invite_body.title", + "translation": "{{ .SenderName }} Vás pozval k připojení se do týmu {{ .TeamDisplayName }}." + }, + { + "id": "api.team.remove_user_from_team.missing.app_error", + "translation": "Uživatel se nezdá být součástí tohoto týmu." + }, + { + "id": "api.team.remove_team_icon.get_team.app_error", + "translation": "Došlo k chybě při načítání týmu." + }, + { + "id": "api.team.remove_member.group_constrained.app_error", + "translation": "Nelze odebrat uživatele z kanálu pro omezený Tým." + }, + { + "id": "api.team.move_channel.success", + "translation": "Tento kanál byl přesunut týmem z %v." + }, + { + "id": "api.team.move_channel.post.error", + "translation": "Zpráva o přesunu kanálu se nezdařila." + }, + { + "id": "api.team.leave.left", + "translation": "%v odešel z týmu." + }, + { + "id": "api.team.join_team.post_and_forget", + "translation": "%v vstoupil do týmu." + }, + { + "id": "api.team.is_team_creation_allowed.disabled.app_error", + "translation": "Vytváření týmů bylo zakázáno. Požádejte správce systému o podrobnosti." + }, + { + "id": "api.team.invite_members.no_one.app_error", + "translation": "Nikoho si nepozval." + }, + { + "id": "api.team.invite_members.invalid_email.app_error", + "translation": "Následující e-mailové adresy nepatří do akceptovatelné domény: {{.Addresses}}. Podrobné informace získáte od správce systému." + }, + { + "id": "api.team.invite_members.disabled.app_error", + "translation": "Emailové pozvánky jsou zakázány." + }, + { + "id": "api.team.invite_guests.channel_in_invalid_team.app_error", + "translation": "Kanály v pozvánce musí patřit k týmu v pozvánce." + }, + { + "id": "api.team.invalidate_all_email_invites.app_error", + "translation": "Chyba při zneplatňování emailových pozvánek." + }, + { + "id": "api.team.import_team.unavailable.app_error", + "translation": "Chybná požadavek: velikost souboru není k známa." + }, + { + "id": "api.team.import_team.parse.app_error", + "translation": "Nelze zpracovat vícedílné formuláře." + }, + { + "id": "api.team.import_team.open.app_error", + "translation": "Nelze otevřít soubor." + }, + { + "id": "api.team.import_team.no_import_from.app_error", + "translation": "Chybný požadavek: pole importFrom není k dispozici." + }, + { + "id": "api.team.import_team.no_file.app_error", + "translation": "Žádný soubor pod 'file' v požadavku." + }, + { + "id": "api.team.import_team.integer.app_error", + "translation": "Velikost souboru není celé číslo." + }, + { + "id": "api.team.import_team.array.app_error", + "translation": "Prázdné pole pod 'file' v požadavku." + }, + { + "id": "api.team.get_team_icon.read_file.app_error", + "translation": "Nelze přečíst soubor s ikonou týmu." + }, + { + "id": "api.team.get_team_icon.filesettings_no_driver.app_error", + "translation": "Neplatný název ovladače pro nastavení souboru. Musí být 'local' nebo 'amazons3'." + }, + { + "id": "api.team.get_invite_info.not_open_team", + "translation": "Pozvánka je neplatná, protože tento tým není otevřený." + }, + { + "id": "api.team.get_all_teams.insufficient_permissions", + "translation": "K zobrazení výčtu všech týmů nemáte oprávnění" + }, + { + "id": "api.team.demote_user_to_guest.license.error", + "translation": "vaše licence nepodporuje účty typu Host" + }, + { + "id": "api.team.demote_user_to_guest.disabled.error", + "translation": "Účty typu Host jsou vypnuty." + }, + { + "id": "api.team.add_user_to_team_from_invite.guest.app_error", + "translation": "Hosté se nesmí připojit k týmu pomocí pozvánkového odkazu. Požádejte tým o zaslání e-mailové pozvánky pro hosta." + }, + { + "id": "api.team.add_user_to_team.missing_parameter.app_error", + "translation": "Parametr potřebný pro přidání uživatele do týmu." + }, + { + "id": "api.team.add_user_to_team.added", + "translation": "%v přidal do týmu od %v." + }, + { + "id": "api.team.add_members.user_denied", + "translation": "Tento tým je spravován skupinami. Tento uživatel není členem skupiny, která je synchronizována do tohoto týmu." + }, + { + "id": "api.team.add_members.error", + "translation": "Chyba při přidávání členů do týmu." + }, + { + "id": "api.system.id_loaded.not_available.app_error", + "translation": "Push upozornění nahrané podle identifikátoru (ID) nejsou nakonfigurované nebo podporované tímto serverem." + }, + { + "id": "api.status.user_not_found.app_error", + "translation": "Uživatel nenalezen." + }, + { + "id": "api.slackimport.slack_import.zip.file_too_large", + "translation": "Název souboru {{.Filename}} v zip archivu je příliš dlouhý, není možno ho proto importovat do Slacku.\r\n" + }, + { + "id": "api.slackimport.slack_import.zip.app_error", + "translation": "Nelze otevřít komprimovaný zip soubor Slacku.\r\n" + }, + { + "id": "api.slackimport.slack_import.team_fail", + "translation": "Nepodařilo se získat tým do importu.\r\n" + }, + { + "id": "api.slackimport.slack_import.open.app_error", + "translation": "Nelze otevřít soubor: {{.Filename}}.\r\n" + }, + { + "id": "api.slackimport.slack_import.notes", + "translation": "\r\nPoznámky:\r\n" + }, + { + "id": "api.slackimport.slack_import.note3", + "translation": "- Další chyby lze nalézt v logu serveru.\r\n" + }, + { + "id": "api.slackimport.slack_import.note2", + "translation": "- zprávy Slack botu nejsou momentálně podporovány.\r\n" + }, + { + "id": "api.slackimport.slack_import.note1", + "translation": "- Některé zprávy nemusejí být importovány, protože nebyly podporovány tímto importem.\r\n" + }, + { + "id": "api.slackimport.slack_import.log", + "translation": "Log importu ze Slacku do Mattermostu\r\n" + }, + { + "id": "api.slackimport.slack_add_users.unable_import", + "translation": "Nelze importovat uživatele Slack: {{.Username}}.\r\n" + }, + { + "id": "api.slackimport.slack_add_users.missing_email_address", + "translation": "Uživatel {{.Username}} nemá e-mailovou adresu v exportu Slacku. Používáte {{.Email}} jako zástupný symbol. Uživatel by měl aktualizovat svou e-mailovou adresu po přihlášení do systému.\r\n" + }, + { + "id": "api.slackimport.slack_add_users.merge_existing_failed", + "translation": "Slack uživatel byl propojen s existujícím Mattermost uživatelem se shodným emailem {{.Email}} a uživatelským jménem {{.Username}}, avšak nemohl být přidán do týmu.\r\n" + }, + { + "id": "api.slackimport.slack_add_users.merge_existing", + "translation": "Slack uživatel byl propojen s existujícím Mattermost uživatelem se shodným emailem {{.Email}} a uživatelským jménem {{.Username}}.\r\n" + }, + { + "id": "api.slackimport.slack_add_users.email_pwd", + "translation": "Slack uživatel s emailem {{.Email}} a heslem {{.Password}} byl naimportován.\r\n" + }, + { + "id": "api.slackimport.slack_add_users.created", + "translation": "\r\nVytvořeno uživatelů:\r\n" + }, + { + "id": "api.slackimport.slack_add_channels.merge", + "translation": "Slack kanál {{.DisplayName}} již existuje jako aktivní kanál Mattermostu. Oba kanály byly sloučeny.\r\n" + }, + { + "id": "api.slackimport.slack_add_channels.import_failed", + "translation": "Nepodařilo se importovat Slack kanál {{.DisplayName}}.\r\n" + }, + { + "id": "api.slackimport.slack_add_channels.failed_to_add_user", + "translation": "Do kanálu se nepodařilo přidat Slack uživatele {{.Username}}.\r\n" + }, + { + "id": "api.slackimport.slack_add_channels.added", + "translation": "\r\nKanály přidány:\r\n" + }, + { + "id": "api.slackimport.slack_add_bot_user.unable_import", + "translation": "Nepodařilo se naimportovat Integration/Slack Bot uživatele {{.Username}}.\r\n" + }, + { + "id": "api.slackimport.slack_add_bot_user.email_pwd", + "translation": "Integration/Slack Bot uživatel s emailem {{.Email}} and heslem {{.Password}} byl naimportován.\r\n" + }, + { + "id": "api.server.start_server.starting.critical", + "translation": "Při spuštění serveru se vyskytla chyba, err:%v" + }, + { + "id": "api.server.start_server.rate_limiting_rate_limiter", + "translation": "Nelze inicializovat omezení rychlosti." + }, + { + "id": "api.server.start_server.rate_limiting_memory_store", + "translation": "Nepodařilo se inicializovat rate limiting memory store. Zkontrolujte nastavení MemoryStoreSize v konfiguraci." + }, + { + "id": "api.server.start_server.forward80to443.enabled_but_listening_on_wrong_port", + "translation": "Nelze přesměrovat port 80 na port 443 při poslechu na portu %s: vypněte Forward80To443, pokud používáte proxy server" + }, + { + "id": "api.server.start_server.forward80to443.disabled_while_using_lets_encrypt", + "translation": "Nutno aktivovat Forward80To443 při použití LetsEncrypt" + }, + { + "id": "api.scheme.patch_scheme.license.error", + "translation": "Vaše licence nepodporuje aktualizování opravňovacích schémat" + }, + { + "id": "api.scheme.get_teams_for_scheme.scope.error", + "translation": "Nelze získat týmy do schématu, protože toto schéma není schéma týmu." + }, + { + "id": "api.scheme.get_channels_for_scheme.scope.error", + "translation": "Nelze získat kanály pro schéma, protože dodané schéma není schéma kanálu." + }, + { + "id": "api.scheme.delete_scheme.license.error", + "translation": "Vaše licence nepodporuje mazání opravňovacích schémat" + }, + { + "id": "api.scheme.create_scheme.license.error", + "translation": "Vaše licence nepodporuje vytváření opravňovacích schémat." + }, + { + "id": "api.roles.patch_roles.license.error", + "translation": "Vaše licence nepodporuje pokročilé oprávnění." + }, + { + "id": "api.restricted_system_admin", + "translation": "Tato operace není k dispozici pro systémového správce s omezenými právy." + }, + { + "id": "api.reaction.save_reaction.user_id.app_error", + "translation": "Nemůžete vložit reakci za jiného uživatele." + }, + { + "id": "api.reaction.save_reaction.invalid.app_error", + "translation": "Reakce je neplatná." + }, + { + "id": "api.reaction.save.archived_channel.app_error", + "translation": "Nelze reagovat v archivovaném kanálu." + }, + { + "id": "api.reaction.delete.archived_channel.app_error", + "translation": "Nelze smazat reakci v archivovaném kanálu." + }, + { + "id": "api.push_notifications_ack.message.parse.app_error", + "translation": "Chyba při vytváření push upozornění." + }, + { + "id": "api.push_notifications_ack.forward.app_error", + "translation": "Při odesílání push notifikace s doručenkou došlo k chybě." + }, + { + "id": "api.push_notifications.message.parse.app_error", + "translation": "Chyba při vytváření push upozornění." + }, + { + "id": "api.push_notification.id_loaded.fetch.app_error", + "translation": "Došlo k chybě při získávání push nofikace založené na ID (identifikátoru)." + }, + { + "id": "api.push_notification.id_loaded.default_message", + "translation": "Máte novou zprávu." + }, + { + "id": "api.push_notification.disabled.app_error", + "translation": "Push upozornění jsou vypnuty na tomto servru." + }, + { + "id": "api.preference.update_preferences.set.app_error", + "translation": "Nelze nastavit uživatelské předvolby." + }, + { + "id": "api.preference.preferences_category.get.app_error", + "translation": "Nelze získat uživatelské předvolby." + }, + { + "id": "api.preference.delete_preferences.delete.app_error", + "translation": "Nelze smazat uživatelské předvolby." + }, + { + "id": "api.post_get_post_by_id.get.app_error", + "translation": "Nepodařilo se načíst příspěvek." + }, + { + "id": "api.post.update_post.system_message.app_error", + "translation": "Nelze aktualizovat systémovou zprávu." + }, + { + "id": "api.post.update_post.permissions_time_limit.app_error", + "translation": "Úprava příspěvku je povolena pouze po dobu {{.timeLimit}} vteřin. Pro více detailů se prosím zeptejte vašeho systémového administrátora." + }, + { + "id": "api.post.update_post.permissions_details.app_error", + "translation": "{{.PostId}} byl již smazán." + }, + { + "id": "api.post.update_post.find.app_error", + "translation": "Nelze najít existující příspěvek, nebo komentář k aktualizaci." + }, + { + "id": "api.post.update_post.can_not_update_post_in_deleted.error", + "translation": "Není možné upravovat zprávu ve smazaném kanálu." + }, + { + "id": "api.post.send_notifications_and_forget.push_message", + "translation": "vám poslal zprávu." + }, + { + "id": "api.post.send_notifications_and_forget.push_image_only", + "translation": " přiložil/a soubor." + }, + { + "id": "api.post.send_notifications_and_forget.push_general_message", + "translation": " napsal/a zprávu." + }, + { + "id": "api.post.send_notifications_and_forget.push_explicit_mention", + "translation": " vás zmínil/a." + }, + { + "id": "api.post.send_notification_and_forget.push_comment_on_thread", + "translation": " komentoval vlákno, ve kterém jste zmíněni." + }, + { + "id": "api.post.send_notification_and_forget.push_comment_on_post", + "translation": " okomentoval/a váš příspěvek." + }, + { + "id": "api.post.send_notification_and_forget.push_channel_mention", + "translation": " upozornil/a kanál." + }, + { + "id": "api.post.patch_post.can_not_update_post_in_deleted.error", + "translation": "Nepodařilo se upravit zprávu ve smazaném kanálu." + }, + { + "id": "api.post.link_preview_disabled.app_error", + "translation": "Náhledy odkazů byly deaktivovány správcem systému." + }, + { + "id": "api.post.get_message_for_notification.images_sent", + "translation": { + "few": "{{.Count}} obrázek odeslán: {{.Filenames}}", + "one": "{{.Count}} obrázků odesláno: {{.Filenames}}", + "other": "{{.Count}} obrázků odesláno: {{.Filenames}}" + } + }, + { + "id": "api.post.get_message_for_notification.files_sent", + "translation": { + "few": "{{.Count}} souborů odeslán: {{.Filenames}}", + "one": "{{.Count}} soubor odeslán: {{.Filenames}}", + "other": "{{.Count}} souborů odesláno: {{.Filenames}}" + } + }, + { + "id": "api.post.do_action.action_integration.app_error", + "translation": "Chyba integrační akce." + }, + { + "id": "api.post.do_action.action_id.app_error", + "translation": "Neplatná id akce." + }, + { + "id": "api.post.disabled_here", + "translation": "@here byl deaktivován, protože kanál má více než {{.Users}} uživatelů." + }, + { + "id": "api.post.disabled_channel", + "translation": "@channel byl deaktivován, protože kanál má více než {{.Users}} uživatelů." + }, + { + "id": "api.post.disabled_all", + "translation": "@all byl deaktivován, protože kanál má více než {{.Users}} uživatelů." + }, + { + "id": "api.post.delete_post.can_not_delete_post_in_deleted.error", + "translation": "Není možné smazat zprávu ve smazaném kanálu." + }, + { + "id": "api.post.deduplicate_create_post.pending", + "translation": "Odeslání bylo odmítnuto, protože jiný klient provedl stejný požadavek." + }, + { + "id": "api.post.deduplicate_create_post.failed_to_get", + "translation": "Nepodařilo se načíst původní příspěvek, po deduplikaci klient vrátil stejný požadavek." + }, + { + "id": "api.post.create_webhook_post.creating.app_error", + "translation": "Chyba při vytváření příspěvku." + }, + { + "id": "api.post.create_post.root_id.app_error", + "translation": "Neplatný parametr RootId." + }, + { + "id": "api.post.create_post.channel_root_id.app_error", + "translation": "Neplatný parametr ChannelId pro parametr RootId." + }, + { + "id": "api.post.create_post.can_not_post_to_deleted.error", + "translation": "Nelze zaslat příspěvek do smazáného kanálu." + }, + { + "id": "api.post.check_for_out_of_channel_mentions.message.one", + "translation": "@{{.Username}} nebyl upozorněnt, protože není členem kanálu." + }, + { + "id": "api.post.check_for_out_of_channel_mentions.message.multiple", + "translation": "@ {{.Usernames}} a @ {{.LastUsername}} nebyli upozorněni, protože nejsou členi kanálu." + }, + { + "id": "api.post.check_for_out_of_channel_groups_mentions.message.one", + "translation": "@ {{.Username}} nedostal upozornění, protože není členem kanálu. Nelze je přidat do kanálu, protože není členem propojených skupin. Chcete-li ho přidat do tohoto kanálu, musí být přidán do propojených skupin." + }, + { + "id": "api.post.check_for_out_of_channel_groups_mentions.message.multiple", + "translation": "@{{.Usernames}} a @{{.LastUsername}} nebyli upozorněni na zmínku, protože nejsou členy tohoto kanálu. Nemohou být přidáni do kanálu, protože nejsou členy připojených skupin. Pro přidání členů do kanálu musejí být přidání do připojených skupin." + }, + { + "id": "api.plugin.verify_plugin.app_error", + "translation": "Nelze ověřit podpis zásuvného modulu." + }, + { + "id": "api.plugin.upload.no_file.app_error", + "translation": "Soubor v multipart/form požadavku nebyl nalezen." + }, + { + "id": "api.plugin.upload.file.app_error", + "translation": "Nelze otevřít soubor uvedený v multipart/form požadavku." + }, + { + "id": "api.plugin.upload.array.app_error", + "translation": "V požadavku typu multipart/form je prázdné pole souborů." + }, + { + "id": "api.plugin.install.download_failed.app_error", + "translation": "Při stahování zásuvného modulu nastala chyba." + }, + { + "id": "model.config.is_valid.message_export.export_from.app_error", + "translation": "Úloha pro export zpráv ExportFromTimestamp musí být čas (vyjádřeno v sekundách od doby unixové). Exportovány budou pouze zprávy odeslané po tomto čase." + }, + { + "id": "model.config.is_valid.message_export.enable.app_error", + "translation": "Nastavení Úlohy exportu zprávy EnableExport musí být \"true\"nebo \"false\"." + }, + { + "id": "model.config.is_valid.message_export.daily_runtime.app_error", + "translation": "Úloha exportu zprávy DailyRuntime musí být časový údaj v 24hodinovém formátu ve tvaru HH: MM." + }, + { + "id": "model.config.is_valid.message_export.batch_size.app_error", + "translation": "Parametr exportu zpráv BatchSize musí být kladné celé číslo." + }, + { + "id": "model.config.is_valid.max_users.app_error", + "translation": "Neplatné hodnota nastavení maximálního počtu uživatelů v týmu. Musí být kladné číslo." + }, + { + "id": "model.config.is_valid.max_notify_per_channel.app_error", + "translation": "Neplatné hodnota nastavení maximálního počtu notifikací per kanál pro týmy. Musí být kladné číslo." + }, + { + "id": "model.config.is_valid.max_file_size.app_error", + "translation": "Neplatná maximální velikost souboru. Musí být celé číslo větší než 0." + }, + { + "id": "model.config.is_valid.max_channels.app_error", + "translation": "Neplatné hodnota nastavení maximálního počtu kanálů pro týmy. Musí být kladné číslo." + }, + { + "id": "model.config.is_valid.max_burst.app_error", + "translation": "Maximální velikost burstu musí být větší než nula." + }, + { + "id": "model.config.is_valid.login_attempts.app_error", + "translation": "Neplatná hodnota pro maximální počet pokusů o přihlášení. Musí být kladné číslo." + }, + { + "id": "model.config.is_valid.listen_address.app_error", + "translation": "Neplatné nastavení adresy pro umístění služby. musí být nastavena." + }, + { + "id": "model.config.is_valid.ldap_username", + "translation": "AD/LDAP pole \"Username Attribute\" je povinné." + }, + { + "id": "model.config.is_valid.ldap_sync_interval.app_error", + "translation": "Neplatný čas synchronizace. Musí být alespoň jedna minuta." + }, + { + "id": "model.config.is_valid.ldap_server", + "translation": "Pole AD/LDAP \"Server AD/LDAP\" je vyžadováno." + }, + { + "id": "model.config.is_valid.ldap_security.app_error", + "translation": "Neplatné nastavení zabezpečeného připojení AD/LDAP. Musí být '', 'TLS' nebo 'STARTTLS'." + }, + { + "id": "model.config.is_valid.ldap_max_page_size.app_error", + "translation": "Neplatná hodnota maximální velikosti stránky." + }, + { + "id": "model.config.is_valid.ldap_login_id", + "translation": "AD/LDAP pole \"Login ID Attribute\" je povinné." + }, + { + "id": "app.import.validate_post_import_data.team_missing.error", + "translation": "Chybějící požadovaná vlastnost příspěvku: Tým." + }, + { + "id": "app.import.validate_post_import_data.message_missing.error", + "translation": "Chybí požadovaná vlastnost příspěvku: Message." + }, + { + "id": "app.import.validate_post_import_data.message_length.error", + "translation": "Položka Message v požadavku je delší než je maximální povolená délka." + }, + { + "id": "app.import.validate_post_import_data.create_at_zero.error", + "translation": "Položka CreateAt v požadavku nesmí být nula." + }, + { + "id": "app.import.validate_post_import_data.create_at_missing.error", + "translation": "Chybějící požadovaná vlastnost Požadavku: create_at." + }, + { + "id": "app.import.validate_post_import_data.channel_missing.error", + "translation": "Chybí požadovaná vlastnost zprávy: Kanál." + }, + { + "id": "app.import.validate_emoji_import_data.name_missing.error", + "translation": "Položka name importovaného smajlíku chybí nebo je prázdná." + }, + { + "id": "app.import.validate_emoji_import_data.image_missing.error", + "translation": "Importovaný smajlík chybí nebo je prázdný." + }, + { + "id": "app.import.validate_emoji_import_data.empty.error", + "translation": "Importovaná data emoji jsou prázdná." + }, + { + "id": "app.import.validate_direct_post_import_data.user_missing.error", + "translation": "Chybí požadovaná vlastnost přímé zprávy: user" + }, + { + "id": "app.import.validate_direct_post_import_data.unknown_flagger.error", + "translation": "Přímý příspěvek může být označen pouze členy tohoto kanálu. \"{{.Username}}\" není členem." + }, + { + "id": "app.import.validate_direct_post_import_data.message_missing.error", + "translation": "Chybí požadovaná vlastnost přímé zprávy: message" + }, + { + "id": "app.import.validate_direct_post_import_data.message_length.error", + "translation": "Zpráva je příliš dlouhá" + }, + { + "id": "app.import.validate_direct_post_import_data.create_at_zero.error", + "translation": "CreateAt musí být větší než 0" + }, + { + "id": "app.import.validate_direct_post_import_data.create_at_missing.error", + "translation": "Chybí požadovaná vlastnost přímé zprávy: create_at" + }, + { + "id": "app.import.validate_direct_post_import_data.channel_members_too_many.error", + "translation": "Seznam členů přímého příspěvku obsahuje příliš mnoho položek" + }, + { + "id": "app.import.validate_direct_post_import_data.channel_members_too_few.error", + "translation": "Seznam členů přímého příspěvku obsahuje příliš málo položek" + }, + { + "id": "app.import.validate_direct_post_import_data.channel_members_required.error", + "translation": "Chybí požadovaná vlastnost přímé zprávy: channel_members" + }, + { + "id": "app.import.validate_direct_channel_import_data.unknown_favoriter.error", + "translation": "Přímý kanál může být zařazen mezi oblíbené pouze jeho členy. \"{{.Username}}\" není členem." + }, + { + "id": "app.import.validate_direct_channel_import_data.members_too_many.error", + "translation": "Seznam členů přímého kanálu obsahuje příliš mnoho položek" + }, + { + "id": "app.import.validate_direct_channel_import_data.members_too_few.error", + "translation": "Seznam členů přímého kanálu obsahuje příliš málo položek" + }, + { + "id": "app.import.validate_direct_channel_import_data.members_required.error", + "translation": "Chybí požadovaná vlastnost přímého kanálu: účastníci" + }, + { + "id": "app.import.validate_direct_channel_import_data.header_length.error", + "translation": "Název přímého kanálu je příliš dlouhý" + }, + { + "id": "app.import.validate_channel_import_data.type_missing.error", + "translation": "Chybí požadovaná vlastnost kanálu: typ." + }, + { + "id": "app.import.validate_channel_import_data.type_invalid.error", + "translation": "Typ kanálu je neplatný." + }, + { + "id": "app.import.validate_channel_import_data.team_missing.error", + "translation": "Chybí požadovaná položka kanálu: team" + }, + { + "id": "app.import.validate_channel_import_data.scheme_invalid.error", + "translation": "Špatný název schématu pro kanál." + }, + { + "id": "app.import.validate_channel_import_data.purpose_length.error", + "translation": "Účel kanálu je příliš dlouhý." + }, + { + "id": "app.import.validate_channel_import_data.name_missing.error", + "translation": "Chybějící požadovaná vlastnost kanálu: jméno" + }, + { + "id": "app.import.validate_channel_import_data.name_length.error", + "translation": "Název kanálu je příliš dlouhý." + }, + { + "id": "app.import.validate_channel_import_data.name_characters.error", + "translation": "Jméno kanálu obsahuje neplatné znaky." + }, + { + "id": "app.import.validate_channel_import_data.header_length.error", + "translation": "Hlavička kanálu je příliš dlouhá." + }, + { + "id": "api.outgoing_webhook.disabled.app_error", + "translation": "Odchozí webhooky byly zakázány systémovým administrátorem." + }, + { + "id": "api.oauth.singup_with_oauth.invalid_link.app_error", + "translation": "Tento registrační odkaz není platný." + }, + { + "id": "api.oauth.singup_with_oauth.expired_link.app_error", + "translation": "Platnost odkazu na registraci vypršela." + }, + { + "id": "api.oauth.singup_with_oauth.disabled.app_error", + "translation": "Registrace uživatele je zakázána." + }, + { + "id": "api.oauth.revoke_access_token.get.app_error", + "translation": "Chyba při získávání tokenu z databáze před smazáním." + }, + { + "id": "api.oauth.revoke_access_token.del_token.app_error", + "translation": "Chyba při mazání tokenu z databáze DB." + }, + { + "id": "api.oauth.revoke_access_token.del_session.app_error", + "translation": "Chyba při mazání sezení z databáze DB." + }, + { + "id": "api.oauth.register_oauth_app.turn_off.app_error", + "translation": "Správce systému vypnul poskytovatele služeb OAuth2." + }, + { + "id": "api.oauth.invalid_state_token.app_error", + "translation": "Neplatný token stavu." + }, + { + "id": "api.oauth.get_access_token.refresh_token.app_error", + "translation": "Invalid_grant: neplatný token pro obnovení." + }, + { + "id": "api.oauth.get_access_token.redirect_uri.app_error", + "translation": "invalid_request: Dané redirect_uri neodpovídá autorizačnímu kódu redirect_uri." + }, + { + "id": "api.oauth.get_access_token.missing_refresh_token.app_error", + "translation": "invalid_request: Chybí refresh_token." + }, + { + "id": "api.oauth.get_access_token.missing_code.app_error", + "translation": "invalid_request: Chybí kód." + }, + { + "id": "api.oauth.get_access_token.internal_user.app_error", + "translation": "server_error: Došlo k chybě při čtení uživatských dat z databáze." + }, + { + "id": "api.oauth.get_access_token.internal_session.app_error", + "translation": "server_error: Došlo k chybě při ukládání sezení do databáze." + }, + { + "id": "api.oauth.get_access_token.internal_saving.app_error", + "translation": "server_error: Došlo k chybě při ukládání tokenu do databáze." + }, + { + "id": "api.oauth.get_access_token.internal.app_error", + "translation": "server_error: Došlo k chybě při přístupu k databázi." + }, + { + "id": "api.oauth.get_access_token.expired_code.app_error", + "translation": "Invalid_grant: Neplatný nebo vypršený autorizační kód." + }, + { + "id": "api.oauth.get_access_token.disabled.app_error", + "translation": "Správce systému vypnul poskytovatele služeb OAuth2." + }, + { + "id": "api.oauth.get_access_token.credentials.app_error", + "translation": "invalid_client: Neplatné uživatelské údaje." + }, + { + "id": "api.oauth.get_access_token.bad_grant.app_error", + "translation": "invalid_request: Špatný grant_type." + }, + { + "id": "api.oauth.get_access_token.bad_client_secret.app_error", + "translation": "invalid_request: Chybí client_secret." + }, + { + "id": "api.oauth.get_access_token.bad_client_id.app_error", + "translation": "invalid_request: Špatné client_id." + }, + { + "id": "api.oauth.authorize_oauth.disabled.app_error", + "translation": "Správce systému vypnul poskytovatele služeb OAuth2." + }, + { + "id": "api.oauth.allow_oauth.turn_off.app_error", + "translation": "Správce systému vypnul poskytovatele služeb OAuth2." + }, + { + "id": "api.oauth.allow_oauth.redirect_callback.app_error", + "translation": "invalid_request: Zadané redirect_uri neodpovídá registrované callback_url." + }, + { + "id": "api.marshal_error", + "translation": "Chyba serializace" + }, + { + "id": "api.license.remove_expired_license.failed.error", + "translation": "E-mail o deaktivaci licence se nepodařilo odeslat." + }, + { + "id": "api.license.client.old_format.app_error", + "translation": "Nový formát klientské licence ještě není podporován. Doplňte do dotazu: format=old." + }, + { + "id": "api.license.add_license.unique_users.app_error", + "translation": "Tato licence podporuje pouze {{.Users}} uživatelů, zatímco váš systém má {{.Count}} unikátních uživatelů. Unikátní uživatelé jsou počítáni podle počtu různých emailových adres. Celkový počet uživatelů můžete najít pod Reporty -> Zobrazit statistiky." + }, + { + "id": "api.license.add_license.save_active.app_error", + "translation": "ID aktivní licence nebylo správně uloženo." + }, + { + "id": "api.license.add_license.save.app_error", + "translation": "Licence se neuložila správně." + }, + { + "id": "api.license.add_license.open.app_error", + "translation": "Nelze otevřít soubor s licencí." + }, + { + "id": "api.license.add_license.no_file.app_error", + "translation": "Žádný soubor pod 'license' v žádosti." + }, + { + "id": "api.license.add_license.invalid_count.app_error", + "translation": "Nelze počítat celkový počet jedinečných uživatelů." + }, + { + "id": "api.license.add_license.invalid.app_error", + "translation": "Neplatný licenční soubor." + }, + { + "id": "api.license.add_license.expired.app_error", + "translation": "Licence uplynula nebo ještě nezačala." + }, + { + "id": "api.license.add_license.array.app_error", + "translation": "Prázdné pole pod 'license' v žádosti." + }, + { + "id": "api.ldap_groups.license_error", + "translation": "vaše licence nepodporuje skupiny ldap" + }, + { + "id": "api.ldap_groups.existing_group_name_error", + "translation": "člen skupiny už existuje" + }, + { + "id": "api.ldap_group.not_found", + "translation": "ldap skupina nebyla nalezena" + }, + { + "id": "api.io_error", + "translation": "chyba vstupu/výstupu" + }, + { + "id": "api.incoming_webhook.invalid_username.app_error", + "translation": "Neplatné uživatelské jméno." + }, + { + "id": "api.incoming_webhook.disabled.app_error", + "translation": "Příchozí webhooky byly zakázány systémovým administrátorem." + }, + { + "id": "api.file.upload_file.too_large_detailed.app_error", + "translation": "Soubor(y) nelze nahrát. {{.Length}}B překračuje maximální povolenou velikost {{.Limit}}B." + }, + { + "id": "api.file.upload_file.storage.app_error", + "translation": "Nelze nahrát obrázek. Uložiště obrázků není nakonfigurováno." + }, + { + "id": "api.file.upload_file.read_request.app_error", + "translation": "Soubor(y) nelze nahrát. Chyba při čtení nebo parsování dat." + }, + { + "id": "api.file.upload_file.read_form_value.app_error", + "translation": "Soubor(y) nelze nahrát. Chyba při čtení hodnoty {{.Formname}}." + }, + { + "id": "api.file.upload_file.multiple_channel_ids.app_error", + "translation": "Soubor(y) nelze nahrát. Několik konfliktních channel_ids." + }, + { + "id": "api.file.upload_file.large_image_detailed.app_error", + "translation": "{{.Filename}} rozměry ({{.Width}} x {{.Height}} pixelů) překračují povolené limity." + }, + { + "id": "api.file.upload_file.large_image.app_error", + "translation": "Soubor větší než povolený rozměr nemohl být nahrán: {{.Filename}}" + }, + { + "id": "api.file.upload_file.incorrect_number_of_client_ids.app_error", + "translation": "Soubor(y) nelze nahrát. Máte {{.NumClientIds}} client_ids pro {{.NumFiles}} souborů." + }, + { + "id": "api.file.read_file.reading_local.app_error", + "translation": "Nastala chyba při čtení z lokálního úložiště." + }, + { + "id": "api.file.no_driver.app_error", + "translation": "Není vybrán ovladač souboru." + }, + { + "id": "api.file.get_public_link.no_post.app_error", + "translation": "K souboru nelze zajistit veřejný odkaz. Soubor musí být přiložen ke zprávě, která je čitelná aktuálnímu uživateli." + }, + { + "id": "api.file.get_public_link.disabled.app_error", + "translation": "Vkládání veřejných odkazů ze deaktivované." + }, + { + "id": "api.file.get_file_thumbnail.no_thumbnail.app_error", + "translation": "Souboru nemá náhled." + }, + { + "id": "api.file.get_file_preview.no_preview.app_error", + "translation": "Souboru nemá náhled." + }, + { + "id": "api.file.get_file.public_invalid.app_error", + "translation": "Tento veřejný odkaz se nezdá být v pořádku." + }, + { + "id": "api.file.attachments.disabled.app_error", + "translation": "Přidávání příloh je na tomto serveru zakázáno." + }, + { + "id": "api.emoji.upload.open.app_error", + "translation": "Nelze vytvořit emoji, jelikož vznikla chyba při pokusu otevřít přiložený obrázek." + }, + { + "id": "api.emoji.upload.large_image.too_large.app_error", + "translation": "Emoji nelze vytvořit. Obrázek musí být menší než {{.MaxWidth}} od {{.MaxHeight}}." + }, + { + "id": "api.emoji.upload.large_image.gif_encode_error", + "translation": "Nemohu vytvořit emotikon. Nastala chyba při pokusu o zakódování souboru GIF." + }, + { + "id": "api.emoji.upload.large_image.gif_decode_error", + "translation": "Nemohu vytvořit emotikon. Nastala chyba při pokusu o dékódování souboru GIF." + }, + { + "id": "api.emoji.upload.large_image.encode_error", + "translation": "Nemohu vytvořit emotikon. Nastala chyba při pokusu o zakódování souboru." + }, + { + "id": "api.emoji.upload.large_image.decode_error", + "translation": "Nemohu vytvořit emotikon. Nastala chyba při pokusu o dékódování souboru." + }, + { + "id": "api.emoji.upload.image.app_error", + "translation": "Nemohu vytvořit emotikon. Soubor musí být fomrátu PNG, JPEG, nebo GIF." + }, + { + "id": "api.emoji.storage.app_error", + "translation": "Úložiště dat není řádně nastaveno. Prosím, nastavte S3 úložiště nebo lokální úložiště na serveru." + }, + { + "id": "api.emoji.get_image.read.app_error", + "translation": "Nemohu přečíst soubor s emotikonem." + }, + { + "id": "api.emoji.get_image.decode.app_error", + "translation": "Nedaří se mi převést soubor s obrázkem na emotikon." + }, + { + "id": "api.emoji.disabled.app_error", + "translation": "Vytváření vlastních emotikonů bylo zakázáno systémovým administrátorem." + }, + { + "id": "api.emoji.create.too_large.app_error", + "translation": "Nemohu vytvořit emotikon. Obrázek musí bát menší než 1 MB." + }, + { + "id": "api.emoji.create.parse.app_error", + "translation": "Nemohu vytvořit emotikon. Nerozumím požadavku." + }, + { + "id": "api.emoji.create.other_user.app_error", + "translation": "Neplatná ID uživatele." + }, + { + "id": "api.emoji.create.duplicate.app_error", + "translation": "Nemohu vytvořit emotikon. Stejně pojmenovaný emotikon již existuje." + }, + { + "id": "api.email_batching.send_batched_email_notification.subject", + "translation": { + "few": "[{{.SiteName}}] Nová upozornění k datu {{.Day}}. {{.Month}} {{.Year}}", + "one": "[{{.SiteName}}] Nové upozornění k datu {{.Day}}. {{.Month}} {{.Year}}", + "other": "[{{.SiteName}}] Nová upozornění k datu {{.Day}}. {{.Month}} {{.Year}}" + } + }, + { + "id": "api.email_batching.add_notification_email_to_batch.disabled.app_error", + "translation": "Dávkování emailů bylo správcem systému zakázáno." + }, + { + "id": "api.email_batching.add_notification_email_to_batch.channel_full.app_error", + "translation": "Dávkovací úloha pro emaily přijímacího kanálu byla moc velká. Prosím zvyšte nastavení EmailBatchingBufferSize." + }, + { + "id": "api.create_terms_of_service.empty_text.app_error", + "translation": "Zadejte prosím text vašich vlastních smluvních podmínek." + }, + { + "id": "api.create_terms_of_service.custom_terms_of_service_disabled.app_error", + "translation": "Funkce vlastních smluvních podmínek je vypnutá." + }, + { + "id": "api.context.token_provided.app_error", + "translation": "Sezení není OAuth ale v dotazu byl poskytnut token." + }, + { + "id": "api.context.session_expired.app_error", + "translation": "Chybná session, prosím přihlaste se znova." + }, + { + "id": "api.context.server_busy.app_error", + "translation": "Servr je vytížen. Nekritické služby jsou dočasně nedostupné." + }, + { + "id": "api.context.permissions.app_error", + "translation": "Nemáte potřebná oprávnění." + }, + { + "id": "api.context.mfa_required.app_error", + "translation": "Tento server vyžaduje multi-faktorovou autentizaci." + }, + { + "id": "api.context.invalid_url_param.app_error", + "translation": "Neplatný nebo chybějící parametr {{.Name}} v URL požadavku." + }, + { + "id": "api.context.invalid_token.error", + "translation": "Neplatný token sezení={{.Token}}, err={{.Error}}" + }, + { + "id": "api.context.invalid_param.app_error", + "translation": "Neplatný parametr {{.Name}}." + }, + { + "id": "api.context.invalid_body_param.app_error", + "translation": "Neplatný nebo chybějící {{.Name}} v těle požadavku." + }, + { + "id": "api.context.404.app_error", + "translation": "Sorry jako, takovou stránku nemám." + }, + { + "id": "api.config.update_config.restricted_merge.app_error", + "translation": "Nepodařilo se sloučit konfiguraci." + }, + { + "id": "api.config.client.old_format.app_error", + "translation": "Nový formát klientské konfigurace ještě není podporován. Doplňte do dotazu: format=old." + }, + { + "id": "api.command_shrug.name", + "translation": "pokrčil rameny" + }, + { + "id": "api.command_shrug.hint", + "translation": "[zpráva]" + }, + { + "id": "api.command_shrug.desc", + "translation": "Přidá ¯\\_(ツ)_/¯ k vaší zprávě" + }, + { + "id": "api.command_shortcuts.unsupported.app_error", + "translation": "Funkce zkratek není na vašem zařízení podporována." + }, + { + "id": "api.command_shortcuts.name", + "translation": "Klávesové zkratky" + }, + { + "id": "api.command_shortcuts.desc", + "translation": "Zobrazí seznam klávesových zkratek" + }, + { + "id": "api.command_settings.unsupported.app_error", + "translation": "Změna nastavení není na vašem zařízení podporována." + }, + { + "id": "api.command_settings.name", + "translation": "nastavení" + }, + { + "id": "api.command_settings.desc", + "translation": "Otevřít dialog s uživatelským nastavením" + }, + { + "id": "api.command_search.unsupported.app_error", + "translation": "Funkce vyhledávání není na vašem zařízení podporována." + }, + { + "id": "api.command_search.name", + "translation": "hledej" + }, + { + "id": "api.command_search.hint", + "translation": "[text]" + }, + { + "id": "api.command_search.desc", + "translation": "Vyhledávat ve zprávách" + }, + { + "id": "api.command_remove.user_not_in_channel", + "translation": "{{.Username}} není členem tohoto kanálu." + }, + { + "id": "api.command_remove.permission.app_error", + "translation": "Nemáte dostatečná oprávnění pro odstranění člena." + }, + { + "id": "api.command_remove.name", + "translation": "odstranit" + }, + { + "id": "api.command_remove.missing.app_error", + "translation": "Nelze najít uživatele. Mohl být deaktivován systémovým administrátorem." + }, + { + "id": "api.command_remove.message.app_error", + "translation": "Příkaz /remove nebo /kick vyžaduje zadanou zprávu." + }, + { + "id": "api.command_remove.hint", + "translation": "@[uživatelské jméno]" + }, + { + "id": "api.command_remove.group_constrained_user_denied", + "translation": "Uživatel nemůže být odstraněn z tohoto kanálu, protože je členem skupiny, která je na tento kanál navázána. Pro odstranění uživatele z tohoto kanálu musíte odstranit uživatele ze skupiny svázané s tímto kanálem." + }, + { + "id": "api.command_remove.direct_group.app_error", + "translation": "Nemůžeš odstranit uživatele ze soukromé komunikace." + }, + { + "id": "api.command_remove.desc", + "translation": "Odstraňte uživatele z kanálu" + }, + { + "id": "api.command_open.name", + "translation": "Otevřít" + }, + { + "id": "api.command_online.success", + "translation": "Nyní jste připojen" + }, + { + "id": "api.command_online.name", + "translation": "Připojen" + }, + { + "id": "api.command_online.desc", + "translation": "Nastavení stavu online" + }, + { + "id": "api.command_offline.success", + "translation": "Nyní jste odpojen" + }, + { + "id": "api.command_offline.name", + "translation": "Odpojen" + }, + { + "id": "api.command_offline.desc", + "translation": "Změna stavu na nepřítomný" + }, + { + "id": "api.command_mute.success_unmute_direct_msg", + "translation": "Tento kanál již není nadále ztlumen." + }, + { + "id": "api.command_mute.success_unmute", + "translation": "{{.Channel}} již není nadále ztlumen." + }, + { + "id": "api.command_mute.success_mute_direct_msg", + "translation": "Nebudete dostávat upozornění pro tento kanál, dokud je ztlumení zapnuto." + }, + { + "id": "api.command_mute.success_mute", + "translation": "Nebudete dostávat upozornění pro {{.Channel}}, dokud nevypnete ztlumení." + }, + { + "id": "api.command_mute.not_member.error", + "translation": "Nelze ztlumit kanál {{.Channel}}, protože nejste člen." + }, + { + "id": "api.command_mute.no_channel.error", + "translation": "Nelze najít kanál {{.Channel}}. Použijte prosím [název kanálu](https://about.mattermost.com/default-channel-handle-documentation) k identifikaci kanálů." + }, + { + "id": "api.command_mute.name", + "translation": "Ztlumit" + }, + { + "id": "api.command_mute.hint", + "translation": "~[kanál]" + }, + { + "id": "api.command_mute.error", + "translation": "Nelze najít kanál {{.Channel}}. Použijte prosím [název kanálu](https://about.mattermost.com/default-channel-handle-documentation) k identifikaci kanálů." + }, + { + "id": "api.command_mute.desc", + "translation": "Vypne emailové a push upozornění pro aktuální kanál nebo specifický [channel]." + }, + { + "id": "api.command_msg.permission.app_error", + "translation": "Nemáte dostatečná oprávnění k odeslání soukromé zprávy tomuto uživateli." + }, + { + "id": "api.command_msg.name", + "translation": "zpráva" + }, + { + "id": "api.command_msg.missing.app_error", + "translation": "Nepodařilo se najít uživatele." + }, + { + "id": "api.command_msg.hint", + "translation": "@[uživatel] 'zpráva'" + }, + { + "id": "api.command_msg.fail.app_error", + "translation": "Chyba při odesílání zprávy uživateli." + }, + { + "id": "api.command_msg.dm_fail.app_error", + "translation": "Chyba při odesílání přímé zprávy." + }, + { + "id": "api.command_msg.desc", + "translation": "Poslat uživateli zprávu" + }, + { + "id": "api.command_me.name", + "translation": "O mně" + }, + { + "id": "api.command_me.hint", + "translation": "[zpráva]" + }, + { + "id": "api.command_me.desc", + "translation": "Provést akci" + }, + { + "id": "api.command_logout.name", + "translation": "Odhlásit" + }, + { + "id": "api.command_logout.desc", + "translation": "Odhlásit se z Mattermost" + }, + { + "id": "api.command_leave.name", + "translation": "Opustit" + }, + { + "id": "api.command_leave.fail.app_error", + "translation": "Při odcházení z kanálu došlo k chybě." + }, + { + "id": "api.command_leave.desc", + "translation": "Opustit aktuální kanál" + }, + { + "id": "api.command_kick.name", + "translation": "vykopnout" + }, + { + "id": "api.command_join.name", + "translation": "Připojit se" + }, + { + "id": "api.command_join.missing.app_error", + "translation": "Nepodařilo se najít kanál." + }, + { + "id": "api.command_join.list.app_error", + "translation": "Došlo k chybě při získávání seznamu kanálů." + }, + { + "id": "api.command_join.hint", + "translation": "~[channel]" + }, + { + "id": "api.command_join.fail.app_error", + "translation": "Došlo k chybě při vstupu do kanálu." + }, + { + "id": "api.command_join.desc", + "translation": "Vstoupit do veřejného kanálu" + }, + { + "id": "api.command_invite_people.permission.app_error", + "translation": "Nemáte oprávnění zvát nové uživatele na tento server." + }, + { + "id": "api.command_invite.user_not_in_team.app_error", + "translation": "{{.Username}} není členem tohoto kanálu." + }, + { + "id": "api.command_invite.user_already_in_channel.app_error", + "translation": "{{.User}} je již v kanálu." + }, + { + "id": "api.command_invite.success", + "translation": "{{.User}} přidán do kanálu {{.Channel}}." + }, + { + "id": "api.command_invite.private_channel.app_error", + "translation": "Nelze najít kanál {{.Channel}}. K identifikaci použijte název kanálu." + }, + { + "id": "api.command_invite.permission.app_error", + "translation": "Nemáte dostatečná oprávnění k přidání {{.User}} do {{.Channel}}." + }, + { + "id": "api.command_invite.name", + "translation": "pozvat" + }, + { + "id": "api.command_invite.missing_user.app_error", + "translation": "Nelze najít uživatele. Mohl být pravděpodobně deaktivován správcem." + }, + { + "id": "api.command_invite.missing_message.app_error", + "translation": "Chybí uživatelské jméno a kanál." + }, + { + "id": "api.command_invite.hint", + "translation": "@[username] ~[channel]" + }, + { + "id": "api.command_invite.group_constrained_user_denied", + "translation": "Tento kanál je spravován skupinami. Tento uživatel není členem skupiny, která je synchronizována do tohoto kanálu." + }, + { + "id": "api.command_invite.fail.app_error", + "translation": "Během připojení ke kanálu došlo k chybě." + }, + { + "id": "api.command_invite.directchannel.app_error", + "translation": "Do přímého kanálu zpráv nemůžete nikoho přidat." + }, + { + "id": "api.command_invite.desc", + "translation": "Pozvěte uživatele do kanálu" + }, + { + "id": "api.command_invite.channel.error", + "translation": "Nelze najít kanál {{.Channel}}. Použijte prosím [channel handle](https://about.mattermost.com/default-channel-handle-documentation) k identifikaci kanálů." + }, + { + "id": "api.command_invite.channel.app_error", + "translation": "Nelze načíst tento kanál." + }, + { + "id": "api.command_help.name", + "translation": "Nápověda" + }, + { + "id": "api.command_help.desc", + "translation": "Otevřít stránku s nápovědou Mattermost" + }, + { + "id": "api.command_groupmsg.permission.app_error", + "translation": "Nemáte dostatečné oprávnění k vytvoření nové skupinové zprávy." + }, + { + "id": "api.command_groupmsg.name", + "translation": "zpráva" + }, + { + "id": "api.command_groupmsg.min_users.app_error", + "translation": "Hromadné zprávy jsou omezeny na minimum {{.MinUsers}} uživatelů." + }, + { + "id": "api.command_groupmsg.max_users.app_error", + "translation": "Hromadné zprávy jsou omezeny na maximum {{.MaxUsers}} uživatelů." + }, + { + "id": "api.command_groupmsg.invalid_user.app_error", + "translation": { + "few": "Nelze najít uživatele: {{.Users}}", + "one": "Nelze najít uživatele: {{.Users}}", + "other": "Nelze najít uživatele: {{.Users}}" + } + }, + { + "id": "api.command_groupmsg.hint", + "translation": "@[username1],@[username2] 'zpráva'" + }, + { + "id": "api.command_groupmsg.group_fail.app_error", + "translation": "Chyba při vytváření skupinové zprávy." + }, + { + "id": "api.command_groupmsg.fail.app_error", + "translation": "Chyba při odesílání zprávy uživateli." + }, + { + "id": "api.command_groupmsg.desc", + "translation": "Odešle hromadnou zprávu vybraným uživatelům" + }, + { + "id": "api.command_expand_collapse.fail.app_error", + "translation": "Při rozbalováni náhledů došlo k chybě." + }, + { + "id": "api.command_expand.success", + "translation": "Odkazy na obrázky se nyní standartně rozbalují" + }, + { + "id": "api.command_expand.name", + "translation": "Rozbalit" + }, + { + "id": "api.command_expand.desc", + "translation": "Vypnout automatické sbalování náhledu obrázků" + }, + { + "id": "api.command_echo.name", + "translation": "odezva" + }, + { + "id": "api.command_echo.message.app_error", + "translation": "Příkaz /echo vyžaduje zadanou zprávu." + }, + { + "id": "api.command_echo.hint", + "translation": "'zpráva' [delay in seconds]" + }, + { + "id": "api.command_echo.high_volume.app_error", + "translation": "Vysoké množství požadavků na echo. Nemohu zpracovat požadavek." + }, + { + "id": "api.command_echo.desc", + "translation": "Vypíše zpět text, bude odeslaný z vašeho účtu" + }, + { + "id": "api.command_echo.delay.app_error", + "translation": "Zdržení musí být do 10000 vteřin." + }, + { + "id": "api.command_dnd.success", + "translation": "Je zapnutý režim \"Nerušit\". Nebudete dostávat upozornění pro počítač ani mobilní telefon, dokud režim opět nevypnete." + }, + { + "id": "api.command_dnd.name", + "translation": "dnd" + }, + { + "id": "api.templates.invite_body.button", + "translation": "Připojit se k týmu" + }, + { + "id": "api.templates.email_warning", + "translation": "Pokud jste neudělali tuto změnu, kontaktujte správce." + }, + { + "id": "api.templates.email_organization", + "translation": "Odeslal/a " + }, + { + "id": "api.templates.email_info3", + "translation": "Tým {{.SiteName}}" + }, + { + "id": "api.templates.email_info2", + "translation": "Srdečně zdravím," + }, + { + "id": "api.templates.email_info1", + "translation": "Jakékoli dotazy nám posílejte e-mailem: " + }, + { + "id": "api.templates.email_footer", + "translation": "Chcete-li změnit předvolby upozornění, přihlaste se do stránky svého týmu a přejděte do Nastavení účtu> Upozornění." + }, + { + "id": "api.templates.email_change_verify_subject", + "translation": "[{{ .SiteName }}] Ověřte novou e-mailovou adresu" + }, + { + "id": "api.templates.email_change_verify_body.title", + "translation": "Aktualizovali jste svůj e-mail" + }, + { + "id": "api.templates.email_change_verify_body.info", + "translation": "Chcete-li dokončit aktualizaci vaší e-mailové adresy pro {{.TeamDisplayName}}, klikněte na níže uvedený odkaz a potvrďte, že je to správná adresa." + }, + { + "id": "api.templates.email_change_verify_body.button", + "translation": "Ověřit email" + }, + { + "id": "api.templates.email_change_subject", + "translation": "[{{ .SiteName }}] Vaše e-mailová adresa se změnila" + }, + { + "id": "api.templates.email_change_body.title", + "translation": "Aktualizoval jste svůj email" + }, + { + "id": "api.templates.email_change_body.info", + "translation": "Vaše emailová adresa pro {{.TeamDisplayName}} byla změněna na {{.NewEmail}}." + }, + { + "id": "api.templates.deactivate_subject", + "translation": "[{{ .SiteName }}] Váš účet na {{ .ServerURL }} byl deaktivován" + }, + { + "id": "api.templates.deactivate_body.warning", + "translation": "Pokud tato změna nebyla iniciována vámi, nebo chcete znovu aktivovat svůj účet, kontaktujte svého správce." + }, + { + "id": "api.templates.deactivate_body.title", + "translation": "Váš účet byl deaktivován na {{ .SiteURL }}" + }, + { + "id": "api.templates.deactivate_body.info", + "translation": "Deaktivovali jste svůj účet na {{ .SiteURL }}." + }, + { + "id": "api.team.update_team_scheme.scheme_scope.error", + "translation": "Nelze nastavit schéma týmu, protože dodávané schéma není schéma týmu." + }, + { + "id": "api.team.update_team_scheme.license.error", + "translation": "Vaše licence nepodporuje aktualizaci týmového schématu" + }, + { + "id": "api.team.update_team_member_roles.guest_and_user.app_error", + "translation": "Neplatná aktualizace člena Týmu: Uživatel může být typu Host nebo Uživatel, ale ne obojí." + }, + { + "id": "api.team.update_restricted_domains.mismatch.app_error", + "translation": "Omezení týmu na {{ .Domain }} není povoleno v configuraci systému. Kontaktujte prosím správce systému." + }, + { + "id": "api.team.update_member_roles.not_a_member", + "translation": "Specifikovaný uživatel není čen specifikovaného týmu." + }, + { + "id": "api.team.team_icon.update.app_error", + "translation": "Došlo k chybě při aktualizaci týmové ikony." + }, + { + "id": "api.team.set_team_icon.write_file.app_error", + "translation": "Nepodařilo se uložit týmovou ikonu." + }, + { + "id": "api.team.set_team_icon.too_large.app_error", + "translation": "Nepodařilo se nahrát týmovou ikonu. Soubor je příliš velký." + }, + { + "id": "api.team.set_team_icon.storage.app_error", + "translation": "Nelze nahrát týmovou ikonu. Uložiště obrázků není nakonfigurováno." + }, + { + "id": "api.team.set_team_icon.parse.app_error", + "translation": "Nelze zpracovat vícedílné formuláře." + }, + { + "id": "api.team.set_team_icon.open.app_error", + "translation": "Nelze otevřít soubor s obrázkem." + }, + { + "id": "api.team.set_team_icon.no_file.app_error", + "translation": "Žádný soubor pod 'image' v žádosti." + }, + { + "id": "api.team.set_team_icon.get_team.app_error", + "translation": "Došlo k chybě při načítání týmu." + }, + { + "id": "api.team.set_team_icon.encode.app_error", + "translation": "Nelze zakódovat ikonu týmu." + }, + { + "id": "api.team.set_team_icon.decode.app_error", + "translation": "Nelze dekódovat ikonu týmu." + }, + { + "id": "api.team.set_team_icon.array.app_error", + "translation": "Prázdné pole pod 'image' v žádosti." + }, + { + "id": "api.team.search_teams.pagination_not_implemented.public_team_search", + "translation": "Pro vyhledávání pouze veřejných týmů není stránkování implementováno." + }, + { + "id": "api.team.search_teams.pagination_not_implemented.private_team_search", + "translation": "Pro vyhledávání pouze soukromých týmů není stránkování implementováno." + }, + { + "id": "api.team.remove_user_from_team.removed", + "translation": "%v byl odstraněn z týmu." + }, + { + "id": "api.command_dnd.desc", + "translation": "Status \"nerušit\" vypne desktopové a mobilní push upozornění." + }, + { + "id": "api.command_collapse.success", + "translation": "Odkazy na obrázky se standardně sbalují" + }, + { + "id": "api.command_collapse.name", + "translation": "sbalit" + }, + { + "id": "api.command_collapse.desc", + "translation": "Zapnout automatické sbalování náhledu obrázků" + }, + { + "id": "api.command_code.name", + "translation": "kód" + }, + { + "id": "api.command_code.message.app_error", + "translation": "Příkaz /code vyžaduje zadanou zprávu." + }, + { + "id": "api.command_code.hint", + "translation": "[text]" + }, + { + "id": "api.command_code.desc", + "translation": "Zobrazení textu jako bloku kódu" + }, + { + "id": "api.command_channel_rename.update_channel.app_error", + "translation": "Nelze načíst tento kanál." + }, + { + "id": "api.command_channel_rename.too_short.app_error", + "translation": "Název kanálu musí mít {{.Length}} nebo více znaků." + }, + { + "id": "api.command_channel_rename.too_long.app_error", + "translation": "Název kanálu nesmí mít více než {{.Length}} znaků." + }, + { + "id": "api.command_channel_rename.permission.app_error", + "translation": "Nemáte dostatečná oprávnění pro přejmenování kanálu." + }, + { + "id": "api.command_channel_rename.name", + "translation": "přejmenuj" + }, + { + "id": "api.command_channel_rename.message.app_error", + "translation": "Příkaz /rename vyžaduje zadanou zprávu." + }, + { + "id": "api.command_channel_rename.hint", + "translation": "[text]" + }, + { + "id": "api.command_channel_rename.direct_group.app_error", + "translation": "Nelze přejmenovat kanál přímých zpráv." + }, + { + "id": "api.command_channel_rename.desc", + "translation": "Přejmenovat kanál" + }, + { + "id": "api.command_channel_rename.channel.app_error", + "translation": "Nelze načíst tento kanál." + }, + { + "id": "api.command_channel_remove.channel.app_error", + "translation": "Nelze načíst tento kanál." + }, + { + "id": "api.command_channel_purpose.update_channel.app_error", + "translation": "Nelze načíst tento kanál." + }, + { + "id": "api.command_channel_purpose.permission.app_error", + "translation": "Nemáte dostatečné oprávnění pro úpravu účelu tohoto kanálu." + }, + { + "id": "api.command_channel_purpose.name", + "translation": "Účel" + }, + { + "id": "api.command_channel_purpose.message.app_error", + "translation": "Příkaz /purpose vyžaduje zadanou zprávu." + }, + { + "id": "api.command_channel_purpose.hint", + "translation": "[text]" + }, + { + "id": "api.command_channel_purpose.direct_group.app_error", + "translation": "Nelze změnit popis u kanálu přímých zpráv. Místo toho použijte příkaz /header pro nastavení hlavičky." + }, + { + "id": "api.command_channel_purpose.desc", + "translation": "Úprava účelu kanálu" + }, + { + "id": "api.command_channel_purpose.channel.app_error", + "translation": "Nelze načíst tento kanál." + }, + { + "id": "api.command_channel_header.update_channel.app_error", + "translation": "Chyba při pokusu změnit aktuální kanál." + }, + { + "id": "api.command_channel_header.permission.app_error", + "translation": "Nemáte dostatečná práva pro změnu záhlaví kanálu." + }, + { + "id": "api.command_channel_header.name", + "translation": "hlavička" + }, + { + "id": "api.command_channel_header.message.app_error", + "translation": "Text musí být uvozen příkazem /echo." + }, + { + "id": "api.command_channel_header.hint", + "translation": "[text]" + }, + { + "id": "api.command_channel_header.desc", + "translation": "Upravit záhlaví kanálu" + }, + { + "id": "api.command_channel_header.channel.app_error", + "translation": "Nelze načíst tento kanál." + }, + { + "id": "api.command_away.success", + "translation": "Momentálně jsi pryč" + }, + { + "id": "api.command_away.name", + "translation": "pryč" + }, + { + "id": "api.command_away.desc", + "translation": "Nastav status pryč" + }, + { + "id": "api.command.team_mismatch.app_error", + "translation": "Nelze aktualizovat příkazy jiných týmů." + }, + { + "id": "api.command.invite_people.sent", + "translation": "Pozvánky emailem byly odeslány" + }, + { + "id": "api.command.invite_people.no_email", + "translation": "Prosím zadejte jednu nebo více emailových adres" + }, + { + "id": "api.command.invite_people.name", + "translation": "pozvat_lidi" + }, + { + "id": "api.command.invite_people.invite_off", + "translation": "Na serveru je zakázáno zakládání nových uživatelů, pozvánka nebyla poslána" + }, + { + "id": "api.command.invite_people.hint", + "translation": "[jmeno@domena.cz ...]" + }, + { + "id": "api.command.invite_people.fail", + "translation": "Došlo k chybě při odesílání pozvánek" + }, + { + "id": "api.command.invite_people.email_off", + "translation": "Email není nastavený, pozvánka/y nebyly odeslány" + }, + { + "id": "api.command.invite_people.email_invitations_off", + "translation": "E-mailové pozvánky jsou zakázány, nebyly odeslány žádné pozvánky" + }, + { + "id": "api.command.invite_people.desc", + "translation": "Poslat email s pozvánkou do Mattermost teamu" + }, + { + "id": "api.command.execute_command.start.app_error", + "translation": "Trigger příkazu nebyl nalezen." + }, + { + "id": "api.command.execute_command.not_found.app_error", + "translation": "Přepínač '{{.Trigger}}' nebyl nalezen. Pokud chceš poslat zprávu začínající znakem \"/\", vlož před tento znak nejprve mezeru." + }, + { + "id": "api.command.execute_command.failed_resp.app_error", + "translation": "Příkaz aktivovaný '{{.Trigger}}' vrátil odpověď {{.Status}}." + }, + { + "id": "api.command.execute_command.failed_empty.app_error", + "translation": "Příkaz aktivovaný '{{.Trigger}}' vrátil prázdnou odpověď." + }, + { + "id": "api.command.execute_command.failed.app_error", + "translation": "Příkaz aktivovaný '{{.Trigger}}' se nezdařil." + }, + { + "id": "api.command.execute_command.create_post_failed.app_error", + "translation": "Příkazu '{{.Trigger}}' se nepodařilo se odeslat odpověď. Prosím kontaktujte správce systému." + }, + { + "id": "api.command.duplicate_trigger.app_error", + "translation": "Toto klíčové slovo se již používá. Vyberte si prosím jiné slovo." + }, + { + "id": "api.command.disabled.app_error", + "translation": "Příkazy byly zakázány systémovým administrátorem." + }, + { + "id": "api.command.command_post.forbidden.app_error", + "translation": "Specifikovaný uživatel není členem specifikovaného týmu." + }, + { + "id": "api.command.admin_only.app_error", + "translation": "Integrace byly omezeny na administrátory." + }, + { + "id": "api.channel.update_team_member_roles.scheme_role.app_error", + "translation": "Poskytovaná role je řízena schématem, a proto ji nelze přímo uplatnit na člena kanálu." + }, + { + "id": "api.channel.update_team_member_roles.changing_guest_role.app_error", + "translation": "Neplatná aktualizace členství týmu: Nelze ručně přidat nebo odebrat roli návštěvníka." + }, + { + "id": "api.channel.update_channel_scheme.scheme_scope.error", + "translation": "Nelze nastavit schéma kanálu, protože schéma není schéma kanálu." + }, + { + "id": "api.channel.update_channel_scheme.license.error", + "translation": "Vaše licence nepodporuje aktualizaci schéma kanálu" + }, + { + "id": "api.channel.update_channel_privacy.default_channel_error", + "translation": "Výchozí kanál nemůže být nastaven jako soukromý/privátní." + }, + { + "id": "api.channel.update_channel_member_roles.scheme_role.app_error", + "translation": "Poskytovaná role je řízena schématem, a proto ji nelze uplatnit přímo na člena kanálu." + }, + { + "id": "api.channel.update_channel_member_roles.guest_and_user.app_error", + "translation": "Neplatná aktualizace členství v kanále: Uživatel může být návštěvník nebo uživatel, ale ne obojí." + }, + { + "id": "api.channel.update_channel_member_roles.changing_guest_role.app_error", + "translation": "Neplatná zemněna členství kanálu: Nelze ručně přidat nebo odebrat roli návštěvníka." + }, + { + "id": "api.channel.update_channel.typechange.app_error", + "translation": "Typ kanálu nelze změnit." + }, + { + "id": "api.channel.update_channel.tried.app_error", + "translation": "Pokus o neplatnou aktualizaci výchozího kanálu {{.Channel}}." + }, + { + "id": "api.channel.update_channel.deleted.app_error", + "translation": "Kanál byl archivován nebo smazán." + }, + { + "id": "api.channel.restore_channel.unarchived", + "translation": "Uživatel {{.Username}} vyjmul kanál z archivu." + }, + { + "id": "api.channel.restore_channel.restored.app_error", + "translation": "Nelze archivovat kanál. Kanál není archivován." + }, + { + "id": "api.channel.rename_channel.cant_rename_group_messages.app_error", + "translation": "Nelze přejmenovat skupinový kanál." + }, + { + "id": "api.channel.rename_channel.cant_rename_direct_messages.app_error", + "translation": "Nelze přejmenovat kanál přímách zpráv." + }, + { + "id": "api.channel.remove_user_from_channel.app_error", + "translation": "Nelze odebrat uživatele z kanálu tohoto typu." + }, + { + "id": "api.channel.remove_members.denied", + "translation": "Kvůli omezením skupiny nelze odebrat následující členy: {{ .UserIDs }}" + }, + { + "id": "api.channel.remove_member.removed", + "translation": "%v byl odstraněn z kanálu." + }, + { + "id": "api.channel.remove_member.group_constrained.app_error", + "translation": "Nelze odebrat uživatele z kanálu s omezenou skupinou." + }, + { + "id": "api.channel.remove_channel_member.type.app_error", + "translation": "Uživatel nemohl být odebrán z kanálu." + }, + { + "id": "api.channel.remove.default.app_error", + "translation": "Nelze odstranit uživatele z výchozího kanálu {{.Channel}}." + }, + { + "id": "api.channel.post_user_add_remove_message_and_forget.error", + "translation": "Nepodařilo se odeslat zprávu o vstupu/odchodu do/z kanálu" + }, + { + "id": "api.channel.post_update_channel_header_message_and_forget.updated_to", + "translation": "%s změnil záhlaví kanálu na: %s" + }, + { + "id": "api.channel.post_update_channel_header_message_and_forget.updated_from", + "translation": "%s změnil záhlaví kanálu z: %s na: %s" + }, + { + "id": "api.channel.post_update_channel_header_message_and_forget.retrieve_user.error", + "translation": "Nepodařilo se načíst uživatele při aktualizaci záhlaví kanálu" + }, + { + "id": "api.channel.post_update_channel_header_message_and_forget.removed", + "translation": "%s odstranil záhlaví kanálu (was: %s)" + }, + { + "id": "api.channel.post_update_channel_header_message_and_forget.post.error", + "translation": "Nepodařilo se odeslat zprávu o aktualizaci záhlaví kanálu" + }, + { + "id": "api.channel.post_update_channel_displayname_message_and_forget.updated_from", + "translation": "%s aktualizován zobrazovaný název kanálu z: %s do: %s" + }, + { + "id": "api.channel.post_update_channel_displayname_message_and_forget.retrieve_user.error", + "translation": "Nepodařilo se načíst uživatele při aktualizaci kanálu DisplayName pole" + }, + { + "id": "api.channel.post_update_channel_displayname_message_and_forget.create_post.error", + "translation": "Nepodařilo se odeslat displayname aktualizace zprávy" + }, + { + "id": "api.channel.post_channel_privacy_message.error", + "translation": "Chyba při updatu zprávy." + }, + { + "id": "api.channel.patch_update_channel.forbidden.app_error", + "translation": "Nepodařilo se aktualizovat kanál." + }, + { + "id": "api.channel.patch_channel_moderations.license.error", + "translation": "Vaše licence nepodporuje moderování kanálů" + }, + { + "id": "api.channel.leave.left", + "translation": "%v opustil kanál." + }, + { + "id": "api.channel.leave.last_member.app_error", + "translation": "Jste poslední člen, místo odcházení zkuste odebrat soukromý kanál." + }, + { + "id": "api.channel.leave.direct.app_error", + "translation": "Nelze opustit kanál přímých zpráv." + }, + { + "id": "api.channel.leave.default.app_error", + "translation": "Nelze opustit výchozí kanál {{.Channel}}." + }, + { + "id": "api.channel.join_channel.post_and_forget", + "translation": "%v se připojil ke kanálu." + }, + { + "id": "api.channel.join_channel.permissions.app_error", + "translation": "Nemáte potřebná oprávnění." + }, + { + "id": "api.channel.guest_join_channel.post_and_forget", + "translation": "%v se pripojil ke kanálu jako host." + }, + { + "id": "api.channel.get_channel_moderations.license.error", + "translation": "Vaše licence nepodporuje moderaci kanálů" + }, + { + "id": "api.channel.delete_channel.type.invalid", + "translation": "Nelze smazat přímou nebo skupinovou zprávu v kanálu" + }, + { + "id": "api.channel.delete_channel.deleted.app_error", + "translation": "Kanál byl archivován nebo smazán." + }, + { + "id": "api.channel.delete_channel.cannot.app_error", + "translation": "Nelze smazat výchozí kanál {{.Channel}}." + }, + { + "id": "api.channel.delete_channel.archived", + "translation": "%v archivoval tento kanál." + }, + { + "id": "api.channel.create_group.bad_user.app_error", + "translation": "Jeden z daných uživatelů neexistuje." + }, + { + "id": "api.channel.create_group.bad_size.app_error", + "translation": "Skupinové kanály musí mít alespoň 3 a maximálně 8 členů." + }, + { + "id": "api.channel.create_direct_channel.invalid_user.app_error", + "translation": "Neplatná ID uživatele pro vytvoření přímého kanálu." + }, + { + "id": "api.channel.create_default_channels.town_square", + "translation": "Náměstí" + }, + { + "id": "api.channel.create_default_channels.off_topic", + "translation": "Mimo téma" + }, + { + "id": "api.channel.create_channel.max_channel_limit.app_error", + "translation": "Nelze vytvořit více než {{.MaxChannelsPerTeam}} kanálů pro aktuální tým." + }, + { + "id": "api.channel.create_channel.direct_channel.app_error", + "translation": "K vytvoření přímého kanálu zpráv musí být použita API služba createDirectChannel." + }, + { + "id": "api.channel.channel_member_counts_by_group.license.error", + "translation": "Vaše licence nepodporuje skupiny" + }, + { + "id": "api.channel.change_channel_privacy.public_to_private", + "translation": "Tento kanál byl převeden na soukromý." + }, + { + "id": "api.channel.change_channel_privacy.private_to_public", + "translation": "Tento kanál byl převeden na veřejný a připojit se může jakýkoli člen." + }, + { + "id": "api.channel.add_user_to_channel.type.app_error", + "translation": "Nelze přidat uživatele do kanálu tohoto typu." + }, + { + "id": "api.channel.add_user.to.channel.failed.deleted.app_error", + "translation": "Nepodařilo se přidat uživatele do kanálu, protože byl odebrán z týmu." + }, + { + "id": "api.channel.add_user.to.channel.failed.app_error", + "translation": "Nepodařilo se přidat uživatele do kanálu." + }, + { + "id": "api.channel.add_members.user_denied", + "translation": "Kvůli omezením skupiny nelze přidat následující členy: {{ .UserIDs }}" + }, + { + "id": "api.channel.add_members.error", + "translation": "Chyba při přidávání členů do kanálu." + }, + { + "id": "api.channel.add_member.added", + "translation": "%v byl přidán do kanálu uživatelem %v." + }, + { + "id": "api.channel.add_guest.added", + "translation": "%v přidán % do kanálu uživatelem %v." + }, + { + "id": "api.bot.teams_channels.add_message_mobile", + "translation": "Přidejte mě prosím do týmů a kanálů, ve kterých chcete abych pracoval. Pro tuto akci využijte prohlížeč nebo aplikaci Mattermost Desktop." + }, + { + "id": "api.bot.create_disabled", + "translation": "Vytváření botů bylo zakázáno." + }, + { + "id": "api.admin.upload_brand_image.too_large.app_error", + "translation": "Nepodařilo se nahrát soubor, je příliš velký." + }, + { + "id": "api.admin.upload_brand_image.storage.app_error", + "translation": "Nelze nahrát obrázek. Ukládání snímků není nakonfigurováno." + }, + { + "id": "api.admin.upload_brand_image.parse.app_error", + "translation": "Nelze zpracovat vícedílné formuláře." + }, + { + "id": "api.admin.upload_brand_image.no_file.app_error", + "translation": "Žádný soubor pod 'image' v žádosti." + }, + { + "id": "api.admin.upload_brand_image.array.app_error", + "translation": "Prázdný array pod 'image' v žádosti." + }, + { + "id": "api.admin.test_s3.missing_s3_bucket", + "translation": "S3 Bucket je povinný" + }, + { + "id": "api.admin.test_email.subject", + "translation": "Mattermost - Testování Nastavení E-Mailu" + }, + { + "id": "api.admin.test_email.reenter_password", + "translation": "SMTP server, port nebo uživatelské jméno bylo změněno. Zadejte prosím znovu SMTP heslo pro otestování připojení." + }, + { + "id": "api.admin.test_email.missing_server", + "translation": "SMTP server je povinný" + }, + { + "id": "api.admin.test_email.body", + "translation": "Zdá se, že váš e-mail pro Mattermost je nastaven správně!" + }, + { + "id": "api.admin.saml.set_certificate_from_metadata.missing_content_type.app_error", + "translation": "Chybějící typ obsahu." + }, + { + "id": "api.admin.saml.set_certificate_from_metadata.invalid_content_type.app_error", + "translation": "Neplatný typ obsahu." + }, + { + "id": "api.admin.saml.set_certificate_from_metadata.invalid_body.app_error", + "translation": "Neplatný text certifikátu." + }, + { + "id": "api.admin.saml.not_available.app_error", + "translation": "SAML 2.0 není nakonfigurován, nebo podporován tímto serverem." + }, + { + "id": "api.admin.saml.metadata.app_error", + "translation": "Nastala chyba při sestavování metadat služby." + }, + { + "id": "api.admin.saml.invalid_xml_missing_ssoservices.app_error", + "translation": "V XML chybí uzel popisující služby SSO poskytovatele identity." + }, + { + "id": "api.admin.saml.invalid_xml_missing_keydescriptor.app_error", + "translation": "V XML chybí uzel popisující key deskroptory poskytovatele identity." + }, + { + "id": "api.admin.saml.invalid_xml_missing_idpssodescriptors.app_error", + "translation": "V XML chybí uzel popisující SSO deskriptory poskytovatele identity." + }, + { + "id": "api.admin.saml.failure_save_idp_certificate_file.app_error", + "translation": "Nelze uložit soubor s certifikátem." + }, + { + "id": "api.admin.saml.failure_parse_idp_certificate.app_error", + "translation": "Selhalo parsování matadat informací obdržených od Identitiy providera pro certifikát." + }, + { + "id": "api.admin.saml.failure_get_metadata_from_idp.app_error", + "translation": "Nelze získat metadata z URL poskytovatele identity." + }, + { + "id": "api.admin.remove_certificate.delete.app_error", + "translation": "Došlo k chybě při mazání certifikátu." + }, + { + "id": "api.admin.get_brand_image.storage.app_error", + "translation": "Úložiště pro obrázky není nakonfigurováno." + }, + { + "id": "api.admin.file_read_error", + "translation": "Chyba při čtení logovacího souboru." + }, + { + "id": "api.admin.delete_brand_image.storage.not_found", + "translation": "Nepodařilo se smazat vlastní logo, obrázek nenalezen." + }, + { + "id": "api.admin.add_certificate.saving.app_error", + "translation": "Nelze uložit soubor s certifikátem." + }, + { + "id": "api.admin.add_certificate.open.app_error", + "translation": "Nelze otevřít soubor s certifikátem." + }, + { + "id": "api.admin.add_certificate.no_file.app_error", + "translation": "Požadavek neobsahuje žádný soubor s certifikátem." + }, + { + "id": "api.admin.add_certificate.array.app_error", + "translation": "Požadavek neobsahuje žádný soubor s ‘certifikátem’." + }, + { + "id": "September", + "translation": "Září" + }, + { + "id": "October", + "translation": "Říjen" + }, + { + "id": "November", + "translation": "Listopad" + }, + { + "id": "May", + "translation": "Květen" + }, + { + "id": "March", + "translation": "Březen" + }, + { + "id": "June", + "translation": "Červen" + }, + { + "id": "July", + "translation": "Červenec" + }, + { + "id": "January", + "translation": "Leden" + }, + { + "id": "February", + "translation": "Únor" + }, + { + "id": "December", + "translation": "Prosinec" + }, + { + "id": "August", + "translation": "Srpen" + }, + { + "id": "April", + "translation": "Duben" + }, + { + "id": "app.bot.permanent_delete.internal_error", + "translation": "Bota se nepodařilo trvale odstranit." + }, + { + "id": "app.audit.get.limit.app_error", + "translation": "Byl překročen limit pro stránkování." + }, + { + "id": "api.user.delete_user.not_enabled.app_error", + "translation": "Funkce trvalého odstranění uživatele není povolena. Obraťte se na svého systémového administrátora." + }, + { + "id": "api.user.delete_team.not_enabled.app_error", + "translation": "Funkce trvalého odstranění týmu není povolena. Obraťte se na svého systémového administrátora." + }, + { + "id": "api.templates.warn_metric_ack.footer", + "translation": "S případnými dotazy se obraťte na support@mattermost.com" + }, + { + "id": "api.templates.warn_metric_ack.body.site_url_header", + "translation": "URL stránky: " + }, + { + "id": "api.templates.warn_metric_ack.body.registered_users_header", + "translation": "Celkem aktivních uživatelů: " + }, + { + "id": "api.templates.warn_metric_ack.body.contact_name_header", + "translation": "Kontakt: " + }, + { + "id": "api.templates.warn_metric_ack.body.contact_email_header", + "translation": "E-mail: " + }, + { + "id": "api.templates.remove_expired_license.subject", + "translation": "Licence Mattermost Enterprise byla zastavena." + }, + { + "id": "api.templates.remove_expired_license.body.title", + "translation": "Platnost Vaší licence Enterprise vypršela a některé funkce mohou být vypnuty. Obnovte licenci." + }, + { + "id": "api.templates.remove_expired_license.body.renew_button", + "translation": "Obnovit licenci" + }, + { + "id": "api.team.join_user_to_team.allowed_domains.app_error", + "translation": "Uživatele nelze přidat, protože doména přiřazená tomuto účtu není povolena. Další podrobnosti žádejte od svého systémového administrátora." + }, + { + "id": "api.team.is_team_creation_allowed.domain.app_error", + "translation": "Uživatele nelze přidat, protože doména přiřazená tomuto účtu není povolena. Další podrobnosti žádejte od svého systémového administrátora." + }, + { + "id": "api.team.import_team.unknown_import_from.app_error", + "translation": "Neznámý zdroj importu." + }, + { + "id": "api.server.warn_metric.number_of_active_users_500.notification_title", + "translation": "Škalování Mattermostu" + }, + { + "id": "api.server.warn_metric.email_us", + "translation": "Napište nám e-mail" + }, + { + "id": "api.server.warn_metric.bot_response.notification_success.message", + "translation": "Děkujeme, že jste se obrátili na Mattermost. Brzy se vám ozveme." + }, + { + "id": "api.server.warn_metric.bot_response.notification_failure.message", + "translation": "Zprávu se nepodařilo odeslat." + }, + { + "id": "api.server.warn_metric.bot_response.notification_failure.body", + "translation": "Napište nám e-mail." + }, + { + "id": "api.server.warn_metric.bot_response.mailto_site_url_header", + "translation": "URL stránky: {{.SiteUrl}}" + }, + { + "id": "api.server.warn_metric.bot_response.mailto_registered_users_header", + "translation": "Celkem aktivních uživatelů: {{.NoRegisteredUsers}}" + }, + { + "id": "api.server.warn_metric.bot_response.mailto_footer", + "translation": "S případnými dotazy se obraťte na support@mattermost.com" + }, + { + "id": "api.server.warn_metric.bot_response.mailto_email_header", + "translation": "E-mail: {{.Email}}" + }, + { + "id": "api.server.warn_metric.bot_response.mailto_contact_header", + "translation": "Kontakt: {{.Contact}}" + }, + { + "id": "api.post.check_for_out_of_channel_group_users.message.none", + "translation": "@{{.GroupName}} nemá členy z tohoto týmu" + }, + { + "id": "api.license.request_trial_license.app_error", + "translation": "Zkušební licenci nelze získat, zkuste to znovu nebo se obraťte na support@mattermost.com." + }, + { + "id": "api.license.request-trial.bad-request.terms-not-accepted", + "translation": "Chcete-li požádat o licenci, musíte odsouhlasit Dohodu o vyhodnocování softwaru Mattermost a Zásady ochrany osobních údajů." + }, + { + "id": "api.license.request-trial.bad-request", + "translation": "Požadovaný počet uživatelů je nesprávný." + }, + { + "id": "api.ldap_groups.existing_user_name_error", + "translation": "tento název skupiny už existuje jako uživatelské jméno" + }, + { + "id": "api.ldap_groups.existing_reserved_name_error", + "translation": "tento název skupiny je rezervován" + }, + { + "id": "api.invalid_channel", + "translation": "Kanál uvedený v požadavku nenáleží tomuto uživateli" + }, + { + "id": "api.image.get.app_error", + "translation": "Požadovanou adresu URL obrázku nelze zpracovat." + }, + { + "id": "api.file.upload_file.incorrect_channelId.app_error", + "translation": "Soubor nelze nahrát. Nesprávné ID kanálu: {{.channelId}}" + }, + { + "id": "api.emoji.create.internal_error", + "translation": "server_error: Při vytváření smajlíku došlo k interní chybě serveru." + }, + { + "id": "api.email.send_warn_metric_ack.missing_server.app_error", + "translation": "Je vyžadován SMTP server" + }, + { + "id": "api.config.update_config.clear_siteurl.app_error", + "translation": "URL stránky nelze smazat." + }, + { + "id": "api.command.execute_command.format.app_error", + "translation": "Před klíčovým slovem příkazu chybí lomítko" + }, + { + "id": "api.channel.move_channel.type.invalid", + "translation": "Kanály přímých či skupinových zpráv nelze přesunout" + }, + { + "id": "model.group.name.invalid_length.app_error", + "translation": "Název musí obsahovat 1 až 64 malých alfanumerických znaků." + }, + { + "id": "web.error.unsupported_browser.system_browser_or", + "translation": "nebo" + }, + { + "id": "api.server.warn_metric.bot_response.mailto_subject", + "translation": "Mattermost Kontaktujte nás požadavek" + }, + { + "id": "api.server.warn_metric.bot_response.mailto_diagnostic_id_header", + "translation": "ID diagnózy: {{.DiagnosticId}}" + }, + { + "id": "api.push_notifications.session.expired", + "translation": "Platnost relace vypršela: Chcete-li nadále dostávat oznámení, přihlaste se. Relace na webu {{.siteName}} jsou konfigurovány vaším správcem systému tak, aby vypršely každý(ch) {{.daysCount}} den/dnů." + }, + { + "id": "api.preference.update_preferences.update_sidebar.app_error", + "translation": "Postranní panel nelze aktualizovat, aby odpovídal aktualizovaným předvolbám" + }, + { + "id": "api.preference.delete_preferences.update_sidebar.app_error", + "translation": "Postranní panel nelze aktualizovat, aby odpovídal smazaným předvolbám" + }, + { + "id": "api.post.search_posts.invalid_body.app_error", + "translation": "Tělo požadavku nelze analyzovat." + }, + { + "id": "api.migrate_to_saml.error", + "translation": "Nelze migrovat SAML." + }, + { + "id": "api.job.unable_to_download_job", + "translation": "Tuto úlohu nelze stáhnout" + }, + { + "id": "api.email.send_warn_metric_ack.invalid_warn_metric.app_error", + "translation": "Nelze najít varovnou metriku." + }, + { + "id": "api.email.send_warn_metric_ack.failure.app_error", + "translation": "Nepodařilo se odeslat potvrzovací e-mail správce" + }, + { + "id": "api.context.get_user.app_error", + "translation": "Nelze získat uživatele z relace UserID." + }, + { + "id": "api.config.migrate_config.app_error", + "translation": "Migrace úložiště konfigurace se nezdařila." + }, + { + "id": "api.admin.ldap.not_available.app_error", + "translation": "LDAP není dostupný." + }, + { + "id": "api.admin.add_certificate.parseform.app_error", + "translation": "Chyba při rozboru multipart požadavku" + }, + { + "id": "app.system.warn_metric.bot_displayname", + "translation": "Rádce Mattermost" + }, + { + "id": "web.error.unsupported_browser.system_browser_make_default", + "translation": "Učinit výchozí" + }, + { + "id": "web.error.unsupported_browser.open_system_browser.edge", + "translation": "Open Edge" + }, + { + "id": "web.error.unsupported_browser.min_os_version.windows", + "translation": "Windows 8.1+" + }, + { + "id": "web.error.unsupported_browser.min_os_version.mac", + "translation": "macOS 11+" + }, + { + "id": "web.error.unsupported_browser.min_browser_version.safari", + "translation": "Verze 14.1+" + }, + { + "id": "web.error.unsupported_browser.min_browser_version.firefox", + "translation": "Verze 91+" + }, + { + "id": "web.error.unsupported_browser.min_browser_version.edge", + "translation": "Verze 95+" + }, + { + "id": "web.error.unsupported_browser.min_browser_version.chrome", + "translation": "Verze 106+" + }, + { + "id": "web.error.unsupported_browser.install_guide.mac", + "translation": "Instalační příručka" + }, + { + "id": "web.error.unsupported_browser.install_guide.windows", + "translation": "Instalační příručka" + }, + { + "id": "web.error.unsupported_browser.browser_title.edge", + "translation": "Microsoft Edge" + }, + { + "id": "web.error.unsupported_browser.browser_title.chrome", + "translation": "Google Chrome" + }, + { + "id": "model.command.is_valid.autocomplete_data.app_error", + "translation": "Nevalidní AutocompleteData" + }, + { + "id": "web.error.unsupported_browser.browser_title.safari", + "translation": "Safari" + }, + { + "id": "web.error.unsupported_browser.browser_title.firefox", + "translation": "Firefox" + }, + { + "id": "app.webhooks.get_incoming_by_channel.app_error", + "translation": "Nepodařilo se získat webhooky." + }, + { + "id": "app.webhooks.get_outgoing_by_team.app_error", + "translation": "Nepodařilo se získat webhooky." + }, + { + "id": "app.webhooks.get_outgoing_by_channel.app_error", + "translation": "Nepodařilo se získat webhooky." + }, + { + "id": "app.webhooks.get_outgoing.app_error", + "translation": "Nepodařilo se získat webhook." + }, + { + "id": "app.webhooks.get_incoming_by_user.app_error", + "translation": "Nepodařilo se získat webhook." + }, + { + "id": "app.webhooks.get_incoming.app_error", + "translation": "Nepodařilo se získat webhook." + }, + { + "id": "app.webhooks.permanent_delete_outgoing_by_user.app_error", + "translation": "Nepodařilo se smazat webhook." + }, + { + "id": "app.webhooks.permanent_delete_outgoing_by_channel.app_error", + "translation": "Nepodařilo se smazat webhook." + }, + { + "id": "app.webhooks.permanent_delete_incoming_by_user.app_error", + "translation": "Nepodařilo se smazat webhook." + }, + { + "id": "app.webhooks.permanent_delete_incoming_by_channel.app_error", + "translation": "Nepodařilo se smazat webhook." + }, + { + "id": "app.webhooks.delete_outgoing.app_error", + "translation": "Nepodařilo se smazat webhook." + }, + { + "id": "app.webhooks.delete_incoming.app_error", + "translation": "Nepodařilo se smazat webhook." + }, + { + "id": "app.oauth.save_app.save.app_error", + "translation": "Nepodařilo se uložit aplikaci." + }, + { + "id": "app.command_webhook.try_use.internal_error", + "translation": "Nepodařilo se použít webhook." + }, + { + "id": "app.command_webhook.get.missing", + "translation": "Nepodařilo se najít webhook." + }, + { + "id": "app.command_webhook.get.internal_error", + "translation": "Nepodařilo se získat webhook." + }, + { + "id": "app.command_webhook.create_command_webhook.internal_error", + "translation": "Nepodařilo se uložit CommandWebhook." + }, + { + "id": "app.session.save.existing.app_error", + "translation": "Nepodařilo se aktualizovat stávající sezení." + }, + { + "id": "app.session.save.app_error", + "translation": "Nepodařilo se uložit sezení." + }, + { + "id": "app.session.remove.app_error", + "translation": "Nepodařilo se odstranit sezení." + }, + { + "id": "app.session.analytics_session_count.app_error", + "translation": "Nepodařilo se spočítat sezení." + }, + { + "id": "app.command.updatecommand.internal_error", + "translation": "Nepodařilo se aktualizovat příkaz." + }, + { + "id": "app.command.movecommand.internal_error", + "translation": "Nepodařilo se přesunout příkaz." + }, + { + "id": "app.command.listallcommands.internal_error", + "translation": "Nepodařilo se získat seznam příkazů." + }, + { + "id": "app.command.deletecommand.internal_error", + "translation": "Nepodařilo se smazat příkaz." + }, + { + "id": "app.command.createcommand.internal_error", + "translation": "Nepodařilo se uložit příkaz." + }, + { + "id": "app.recover.save.app_error", + "translation": "Nepodařilo se uložit token." + }, + { + "id": "web.error.unsupported_browser.learn_more", + "translation": "Zjistěte více o podporovaných prohlížečích." + }, + { + "id": "web.error.unsupported_browser.browser_get_latest.safari", + "translation": "Použijte poslední verzi prohlížeče Safari" + }, + { + "id": "web.error.unsupported_browser.browser_get_latest.firefox", + "translation": "Použijte poslední verzi prohlížeče Firefox" + }, + { + "id": "web.error.unsupported_browser.browser_get_latest.chrome", + "translation": "Použijte poslední verzi prohlížeče Chrome" + }, + { + "id": "model.group.description.app_error", + "translation": "nevalidní položka popisu skupiny." + }, + { + "id": "model.config.is_valid.saml_spidentifier_attribute.app_error", + "translation": "Service Provider Identifier je vyžadován" + }, + { + "id": "bleveengine.stop_user_index.error", + "translation": "Selhalo uzavření indexu uživatelů." + }, + { + "id": "bleveengine.stop_post_index.error", + "translation": "Selhalo uzavření indexu příspěvků." + }, + { + "id": "bleveengine.stop_channel_index.error", + "translation": "Selhalo uzavření indexu kanálů." + }, + { + "id": "bleveengine.purge_user_index.error", + "translation": "Selhalo promazání indexu uživatelů." + }, + { + "id": "bleveengine.purge_post_index.error", + "translation": "Selhalo promazání indexu příspěvků." + }, + { + "id": "bleveengine.purge_channel_index.error", + "translation": "Selhalo promazání indexu kanálů." + }, + { + "id": "bleveengine.indexer.do_job.bulk_index_users.batch_error", + "translation": "Selhala indexace dávky uživatelů." + }, + { + "id": "bleveengine.indexer.do_job.bulk_index_posts.batch_error", + "translation": "Selhala indexace dávky příspěvků." + }, + { + "id": "bleveengine.indexer.do_job.bulk_index_channels.batch_error", + "translation": "Selhala indexace dávky kanálů." + }, + { + "id": "bleveengine.delete_user_posts.error", + "translation": "Selhalo smazání příspěvků uživatele" + }, + { + "id": "ent.elasticsearch.delete_user_posts.error", + "translation": "Selhalo smazání příspěvků uživatele" + }, + { + "id": "ent.elasticsearch.delete_channel_posts.error", + "translation": "Selhalo smazání příspěvků v kanálu" + }, + { + "id": "bleveengine.delete_channel_posts.error", + "translation": "Selhalo smazání příspěvků v kanálu" + }, + { + "id": "app.terms_of_service.get.no_rows.app_error", + "translation": "Podmínky služby nebyly nalezeny." + }, + { + "id": "app.user_terms_of_service.get_by_user.no_rows.app_error", + "translation": "Podmínky služby nebyly nalezeny." + }, + { + "id": "app.scheme.save.invalid_scheme.app_error", + "translation": "Poskytnuté schéma není validní." + }, + { + "id": "app.scheme.delete.app_error", + "translation": "Nemohu smazat toto schéma." + }, + { + "id": "app.emoji.get.no_result", + "translation": "Nemohli jsme najít emoji." + }, + { + "id": "app.emoji.get_by_name.no_result", + "translation": "Nemohli jsme najít emoji." + }, + { + "id": "app.emoji.get.app_error", + "translation": "Nemohu získat emoji." + }, + { + "id": "app.emoji.get_list.internal_error", + "translation": "Nemohu získat emoji." + }, + { + "id": "app.emoji.get_by_name.app_error", + "translation": "Nemohu získat emoji." + }, + { + "id": "app.channel.restore.app_error", + "translation": "Nemohu obnovit kanál." + }, + { + "id": "app.channel.get_channels.get.app_error", + "translation": "Nemohu získat kanály." + }, + { + "id": "app.channel.get_channels_by_ids.get.app_error", + "translation": "Nemohu získat kanály." + }, + { + "id": "app.channel.get_more_channels.get.app_error", + "translation": "Nemohu získat kanály." + }, + { + "id": "app.channel.delete.app_error", + "translation": "Nemohu smazat kanál." + }, + { + "id": "app.channel.permanent_delete.app_error", + "translation": "Nemohu smazat kanál." + }, + { + "id": "app.post.get_posts.app_error", + "translation": "Byl překročen limit pro stránkování." + }, + { + "id": "app.upload.create.save.app_error", + "translation": "Chyba při ukládání nahraného souboru." + }, + { + "id": "ent.ldap.do_login.x509.app_error", + "translation": "Chyba při vytváření klíče" + }, + { + "id": "app.role.get_by_names.app_error", + "translation": "Nemohu získat role." + }, + { + "id": "app.role.get.app_error", + "translation": "Nemohu získat roli." + }, + { + "id": "app.role.get_by_name.app_error", + "translation": "Nemohu získat roli." + }, + { + "id": "store.sql_command.update.missing.app_error", + "translation": "Příkaz neexistuje." + }, + { + "id": "store.sql_command.get.missing.app_error", + "translation": "Příkaz neexistuje." + }, + { + "id": "app.user.permanentdeleteuser.internal_error", + "translation": "Nemohu smazat uživatele." + }, + { + "id": "app.team.permanentdeleteteam.internal_error", + "translation": "Nemohu smazat tým." + }, + { + "id": "app.recover.delete.app_error", + "translation": "Nemohu smazat token." + }, + { + "id": "model.link_metadata.is_valid.timestamp.app_error", + "translation": "Timestamp pro link metadata musí být nenulový a zaokrouhlen na nejbližší hodinu." + }, + { + "id": "model.job.is_valid.status.app_error", + "translation": "Nevalidní status úkolu." + }, + { + "id": "model.group_syncable.syncable_id.app_error", + "translation": "Nevalidní syncable id pro skupinové syncable." + }, + { + "id": "api.server.warn_metric.number_of_channels_50.notification_title", + "translation": "Použití Advanced Permissions" + }, + { + "id": "model.link_metadata.is_valid.url.app_error", + "translation": "Je nutné nastavit Link metadata URL." + }, + { + "id": "plugin_api.send_mail.missing_to", + "translation": "Chybí adresa příjemce." + }, + { + "id": "plugin_api.send_mail.missing_subject", + "translation": "Chybí předmět emailu." + }, + { + "id": "plugin_api.send_mail.missing_htmlbody", + "translation": "Chybí HTML obsah." + }, + { + "id": "plugin_api.bot_cant_create_bot", + "translation": "Bot účet nemůže založit jiný bot účet." + }, + { + "id": "plugin.api.update_user_status.bad_status", + "translation": "Nelza nastavit stav uživatele. Neznámý stav." + }, + { + "id": "plugin.api.get_users_in_channel", + "translation": "Nelze získat uživatele, chybná řídící kritéria." + }, + { + "id": "model.plugin_key_value.is_valid.plugin_id.app_error", + "translation": "Chybné ID zásuvného modulu, musí být dlouhé minimálně {{.Min}} a maximálně {{.Max}} znaků." + }, + { + "id": "model.plugin_command_error.error.app_error", + "translation": "Zásuvný modul pro /{{.Command}} nefunguje. Prosím kontaktujte správce" + }, + { + "id": "model.plugin_command_crash.error.app_error", + "translation": "/{{.Command}} způsobil pád zásuvného modulu {{.PluginId}}. Prosím kontaktujte správce" + }, + { + "id": "model.command.is_valid.plugin_id.app_error", + "translation": "Nevalidní id zásuvného modulu." + }, + { + "id": "app.upload.run_plugins_hook.rejected", + "translation": "Nelza nahrát soubor {{.Filename}}. Odmítnuto zásuvným modulem: {{.Reason}}" + }, + { + "id": "app.upload.run_plugins_hook.move_fail", + "translation": "Přesun souboru selhal." + }, + { + "id": "app.plugin_store.save.app_error", + "translation": "Nelze uložit hodnotu klíče zásuvného modulu." + }, + { + "id": "app.plugin_store.list.app_error", + "translation": "Nelze získat seznam klíčů zásuvného modulu." + }, + { + "id": "app.plugin_store.delete.app_error", + "translation": "Nelze odstranit hodnotu klíče zásuvného modulu." + }, + { + "id": "app.plugin_store.get.app_error", + "translation": "Nelze získat hodnotu klíče zásuvného modulu." + }, + { + "id": "api.server.warn_metric.email_domain.start_trial_notification_success.message", + "translation": "Váš Enterprise test je nyní aktivní. Jděte do **Systémová konzole > Autentizace > Návštěvníci** a povolte návštěvnické účty." + }, + { + "id": "api.context.local_origin_required.app_error", + "translation": "Tento endpoint vyžaduje lokální request origin." + }, + { + "id": "api.config.get_config.restricted_merge.app_error", + "translation": "Selhalo sloučení dané konfigurace." + }, + { + "id": "api.cloud.request_error", + "translation": "Chyba zpracování požadavku na CWS." + }, + { + "id": "api.cloud.license_error", + "translation": "Vaše licence neumožňuje cloudové dotazy." + }, + { + "id": "api.cloud.app_error", + "translation": "Interní chyba bězem požadavku na cloud api." + }, + { + "id": "model.group_syncable.group_id.app_error", + "translation": "neplatná položka group id pro group syncable." + }, + { + "id": "model.group.update_at.app_error", + "translation": "neplatná položka update at pro skupinu." + }, + { + "id": "model.group.source.app_error", + "translation": "neplatná položka source pro skupinu." + }, + { + "id": "model.group.remote_id.app_error", + "translation": "neplatná položka remote id pro skupinu." + }, + { + "id": "model.group.name.invalid_chars.app_error", + "translation": "neplatná znaky v položce name pro skupinu" + }, + { + "id": "model.group.name.app_error", + "translation": "neplatné name pro skupinu." + }, + { + "id": "model.group_member.group_id.app_error", + "translation": "neplatné group id pro člena skupiny." + }, + { + "id": "model.group_member.user_id.app_error", + "translation": "neplatné user id pro člena skupiny." + }, + { + "id": "model.link_metadata.is_valid.data.app_error", + "translation": "Data pro link metadata nemohou být neuvedena (nil)." + }, + { + "id": "model.link_metadata.is_valid.data_type.app_error", + "translation": "Data pro link metadata neodpovídají danému typu." + }, + { + "id": "model.link_metadata.is_valid.type.app_error", + "translation": "Nevalidní typ pro link metadata." + }, + { + "id": "model.outgoing_hook.is_valid.callback.app_error", + "translation": "Nevalidní callback URL." + }, + { + "id": "model.outgoing_hook.is_valid.content_type.app_error", + "translation": "Neplatná hodnota pro content_type." + }, + { + "id": "model.plugin_command.error.app_error", + "translation": "Při provádění tohoto příkazu nastala chyba." + }, + { + "id": "model.plugin_key_value.is_valid.key.app_error", + "translation": "Chybný klíč, jeho délka musí být minimálně {{.Min}} a maximálně {{.Max}} znaků." + }, + { + "id": "model.plugin_kvset_options.is_valid.old_value.app_error", + "translation": "Chybná původní hodnota, nesmí být zadána pokud není operace atomická." + }, + { + "id": "model.post.channel_notifications_disabled_in_channel.message", + "translation": "Notifikace jsou zakázány pro kanál {{.ChannelName}}. Zmínka {{.Mention}} nezpůsobila žádnou notifikaci." + }, + { + "id": "model.search_params_list.is_valid.include_deleted_channels.app_error", + "translation": "Všechny parametry IncludeDeletedChannels musí mít stejnou hodnotu." + }, + { + "id": "model.team.is_valid.invite_id.app_error", + "translation": "Nevalidní invite id." + }, + { + "id": "model.team.is_valid.url.app_error", + "translation": "Nevalidní URL." + }, + { + "id": "model.upload_session.is_valid.channel_id.app_error", + "translation": "Nevalidní hodnota pro ChannelId." + }, + { + "id": "model.upload_session.is_valid.create_at.app_error", + "translation": "Nevalidní hodnota pro CreateAt" + }, + { + "id": "model.upload_session.is_valid.file_offset.app_error", + "translation": "Nevalidní hodnota pro FileOffset" + }, + { + "id": "model.upload_session.is_valid.file_size.app_error", + "translation": "Nevalidní hodnota pro FileSize" + }, + { + "id": "model.upload_session.is_valid.filename.app_error", + "translation": "Nevalidní hodnota pro Filename" + }, + { + "id": "model.upload_session.is_valid.id.app_error", + "translation": "Nevalidní hodnota pro Id" + }, + { + "id": "model.upload_session.is_valid.path.app_error", + "translation": "Nevalidní hodnota pro Path" + }, + { + "id": "model.upload_session.is_valid.type.app_error", + "translation": "Nevalidní hodnota pro Type" + }, + { + "id": "model.upload_session.is_valid.user_id.app_error", + "translation": "Nevalidní hodnota pro UserId" + }, + { + "id": "model.user.is_valid.auth_data_type.app_error", + "translation": "Nevalidní uživatel - „auth data“ musí být zadána s „auth type\"." + }, + { + "id": "model.user.is_valid.password_limit.app_error", + "translation": "Nelze použít heslo delší jak 72 znaků díky limitaci funkce bcrypt." + }, + { + "id": "api.server.warn_metric.number_of_channels_50.contact_us.email_body", + "translation": "Dotaz na Mattermost. Mám zájem zjistit více o Pokročilých oprávněních a Systémových schématech.\n" + }, + { + "id": "api.server.warn_metric.number_of_active_users_500.start_trial.notification_success.message", + "translation": "Váš Enterprise test je nyní aktivní. Jděte do Systémové konzole chcete-li povolit rozšířené vlastnosti." + }, + { + "id": "api.server.warn_metric.number_of_active_users_500.start_trial.notification_body", + "translation": "Mattermost silně doporučuje, aby nasazení serveru s více jak 500 uživateli využilo vlastností jako správa uživatelů, server clustering a monitorování výkonu. Kontaktujte nás a zjistěte více o tom, jak můžeme pomoci.\n\nKliknutím na Zahájit test souhlasíte s [Mattermost Software Evaluation Agreement](https://mattermost.com/software-evaluation-agreement/), [Privacy Policy](https://mattermost.com/privacy-policy/) a příjmem produktových emailů." + }, + { + "id": "api.server.warn_metric.number_of_active_users_500.notification_body", + "translation": "Mattermost silně doporučuje, aby nasazení serveru s více jak 500 uživateli využilo vlastností jako správa uživatelů, server clustering a monitorování výkonu. Kontaktujte nás a zjistěte více o tom, jak můžeme pomoci.\n\nKliknutím na Kontaktujte nás budete sdílet své informace s Mattermost, Inc. [Zjistit více](https://mattermost.com/pl/default-admin-advisory)" + }, + { + "id": "api.server.warn_metric.number_of_active_users_500.contact_us.email_body", + "translation": "Dotaz na Mattermost. Můj tým má nyní 500 uživatelů a zvažuji využít Mattermost Enterprise Edition.\n" + }, + { + "id": "api.server.warn_metric.number_of_active_users_300.start_trial.notification_title", + "translation": "Oznamovací kanály" + }, + { + "id": "api.server.warn_metric.number_of_active_users_300.start_trial.notification_success.message", + "translation": "Váš Enterprise test je nyní aktivní. Jděte do **Systémová konzole > Správa uživatelů > Kanály** chcete-li omezit tvorbu zpráv v kanálu." + }, + { + "id": "api.server.warn_metric.number_of_active_users_300.start_trial.notification_body", + "translation": "Při veškeré konverzaci, která se děje napříč Mattermostem, může být obtížné pátrat po důležitých informacích. Pokud chcete rozesílat informace širokému publiku, můžete vytvořit Oznamovací kanály. Ty jsou pouze pro čtení - vyjma adminů, kteří mohou zprávy vytvářet.\n\n[Zjistit více o Oznamovacích kanálech](https://www.mattermost.com/docs-channel-moderation/?utm_medium=product&utm_source=mattermost-advisor-bot&utm_content=channel-moderation)\n\nKliknutím na Zahájit test souhlasíte s [Mattermost Software Evaluation Agreement](https://mattermost.com/software-evaluation-agreement/), [Privacy Policy](https://mattermost.com/privacy-policy/) a příjmem produktových emailů." + }, + { + "id": "api.server.warn_metric.number_of_active_users_300.notification_body", + "translation": "Při veškeré konverzaci, která se děje napříč Mattermostem, může být obtížné pátrat po důležitých informacích. Pokud chcete rozesílat informace širokému publiku, můžete vytvořit Oznamovací kanály. Ty jsou pouze pro čtení - vyjma adminů, kteří mohou zprávy vytvářet.\n\n[Zjistit více o Oznamovacích kanálech](https://www.mattermost.com/docs-channel-moderation/?utm_medium=product&utm_source=mattermost-advisor-bot&utm_content=channel-moderation)\n\nKliknutím na Kontaktujte nás budete sdílet své informace s Mattermost, Inc. [Zjistit více](https://mattermost.com/pl/default-admin-advisory)" + }, + { + "id": "api.server.warn_metric.number_of_active_users_300.contact_us.email_body", + "translation": "Dotaz na Mattermost. Zajímalo by mě více o tvorbě oznamovacích kanálů.\n" + }, + { + "id": "api.server.warn_metric.number_of_active_users_200.start_trial.notification_success.message", + "translation": "Váš Enterprise test je nyní aktivní. Jděte do **Systémová konzole > Autentizace > SAML 2.0** chcete-li integraci se SAML 2.0." + }, + { + "id": "api.server.warn_metric.number_of_active_users_200.start_trial.notification_body", + "translation": "Váš Mattermost má nyní 200 uživatelů. Pokud jej propojíte s vaším řešením SSO (single sign on), uživatelé budou moci využívat Mattermost bez nutnosti znovu zadávat své přihlašovací údaje. Doporučujeme integraci vašeho SAML 2.0 poskytovatele s Mattermost serverem [Zjistit více o integraci se SAML 2.0](https://www.mattermost.com/docs-saml/?utm_medium=product&utm_source=mattermost-advisor-bot&utm_content=saml).\n\nKliknutím na Zahájit test souhlasíte s [Mattermost Software Evaluation Agreement](https://mattermost.com/software-evaluation-agreement/), [Privacy Policy](https://mattermost.com/privacy-policy/) a příjmem produktových emailů." + }, + { + "id": "api.server.warn_metric.number_of_active_users_200.notification_title", + "translation": "Škálování Mattermostu" + }, + { + "id": "api.server.warn_metric.number_of_active_users_200.notification_body", + "translation": "Váš Mattermost má nyní 200 uživatelů. Pokud jej propojíte s vaším řešením SSO (single sign on), uživatelé budou moci využívat Mattermost bez nutnosti znovu zadávat své přihlašovací údaje. Doporučujeme integraci vašeho SAML 2.0 poskytovatele s Mattermost serverem [Zjistit více o integraci se SAML 2.0](https://www.mattermost.com/docs-saml/?utm_medium=product&utm_source=mattermost-advisor-bot&utm_content=saml).\n\nKliknutím na Kontaktujte nás budete sdílet své informace s Mattermost, Inc. [Zjistit více](https://mattermost.com/pl/default-admin-advisory)" + }, + { + "id": "api.server.warn_metric.number_of_active_users_200.contact_us.email_body", + "translation": "Dotaz na Mattermost. Můj tým má nyní 200 uživatelů a zvažuji přejít na Mattermost Enterprise Edition.\n" + }, + { + "id": "api.server.warn_metric.number_of_active_users_100.start_trial.notification_success.message", + "translation": "Váš Enterprise test je nyní aktivní. Jděte do **Systémová konzole > Autentizace > AD/LDAP** chcete-li se napojit na vaše AD/LDAP." + }, + { + "id": "api.server.warn_metric.number_of_active_users_100.start_trial.notification_body", + "translation": "Váš Mattermost má přes 100 uživatelů. S tím, jak roste uživatelská základna, správa nových účtů se stává časově nákladnou. Doporučujeme integrovat vaši podnikovou databázi AD/LDAP, která umožní každému účtu přístup k Mattermostu.\n\n[Zjistit více o integraci ADL/LDAP](https://www.mattermost.com/docs-adldap/?utm_medium=product&utm_source=mattermost-advisor-bot&utm_content=adldap)\n\nKliknutím na Zahájit test souhlasíte s [Mattermost Software Evaluation Agreement](https://mattermost.com/software-evaluation-agreement/), [Privacy Policy](https://mattermost.com/privacy-policy/) a příjmem produktových emailů." + }, + { + "id": "api.server.warn_metric.number_of_active_users_100.notification_title", + "translation": "Škálování s Mattermostem" + }, + { + "id": "api.server.warn_metric.email_domain.notification_body", + "translation": "Projekty často zahrnují lidi jak z vnitřku, tak vnějšku organizace. S pomocí návštěvnických účtů můžete zapojit externí partnery do vašeho Mattermostu a určit, s kým mohou pracovat a co mohou vidět.\n\n[Zjistit více o návštěvnických účtech]((https://www.mattermost.com/docs-guest-accounts/?utm_medium=product&utm_source=mattermost-advisor-bot&utm_content=guest-accounts).\n\nKliknutím na Kontaktujte nás budete sdílet své informace s Mattermost, Inc. [Zjistit více](https://mattermost.com/pl/default-admin-advisory)" + }, + { + "id": "store.sql_bot.get.missing.app_error", + "translation": "Bot účet neexistuje." + }, + { + "id": "bleveengine.already_started.error", + "translation": "Bleve již běží." + }, + { + "id": "app.import.get_users_by_username.some_users_not_found.error", + "translation": "Někteří uživatelé nenalezeni" + }, + { + "id": "app.import.get_teams_by_names.some_teams_not_found.error", + "translation": "Některé týmy nenalezeny" + }, + { + "id": "app.emoji.create.internal_error", + "translation": "Selhalo uložení emoji." + }, + { + "id": "app.channel.update_channel.internal_error", + "translation": "Selhala aktualizace kanálu." + }, + { + "id": "app.channel.get_deleted.missing.app_error", + "translation": "Neexistuje žádný smazaný kanál." + }, + { + "id": "app.post.search.app_error", + "translation": "Chyba při prohledávání příspěvků" + }, + { + "id": "app.channel.count_posts_since.app_error", + "translation": "Nepodařilo se spočítat zprávy od zadaného data." + }, + { + "id": "app.channel.clear_all_custom_role_assignments.select.app_error", + "translation": "Selhalo získání členů v kanálu." + }, + { + "id": "app.channel.autofollow.app_error", + "translation": "Selhala aktualizace členství ve vlákně pro zmíněného uživatele" + }, + { + "id": "app.channel.analytics_type_count.app_error", + "translation": "Selhalo získání počtů typů kanálů." + }, + { + "id": "app.analytics.getanalytics.internal_error", + "translation": "Selhalo získání analytiky." + }, + { + "id": "api.user.update_user_roles.license.app_error", + "translation": "Vlastní Permission Schemes nejsou povoleny současnou licencí" + }, + { + "id": "api.user.update_password.user_and_hashed.app_error", + "translation": "Pouze správci mohou nastavovat hash hesel." + }, + { + "id": "api.user.login_cws.license.error", + "translation": "CWS přihlášení není povoleno." + }, + { + "id": "api.user.login_by_cws.invalid_token.app_error", + "translation": "Nevalidní CWS oken" + }, + { + "id": "api.user.get_uploads_for_user.forbidden.app_error", + "translation": "Selhalo získání nahraných souborů." + }, + { + "id": "api.user.delete_channel.not_enabled.app_error", + "translation": "Trvalé smazání kanálů není povoleno. Obraťte se na svého správce." + }, + { + "id": "api.user.autocomplete_users.missing_team_id.app_error", + "translation": "Parametr Id týmu je vyžadován pro autocomplete kanálu." + }, + { + "id": "api.upload.upload_data.multipart_error", + "translation": "Selhalo zpracování multipart obsahu." + }, + { + "id": "api.upload.upload_data.invalid_content_type", + "translation": "Nevalidní Content-Type pro víceobsahový upload." + }, + { + "id": "api.upload.upload_data.invalid_content_length", + "translation": "Nevalidní Content-Length." + }, + { + "id": "app.upload.get.app_error", + "translation": "Selhalo získání nahraného souboru." + }, + { + "id": "api.upload.get_upload.forbidden.app_error", + "translation": "Selhalo získání nahraného souboru." + }, + { + "id": "api.upgrade_to_enterprise_status.signature.app_error", + "translation": "Mattermost nemohl bý aktualizován na Enterprise edici. Digitální podpis staženého binárního souboru nemohl být ověřen." + }, + { + "id": "api.upgrade_to_enterprise_status.app_error", + "translation": "Mattermost nemohl bý aktualizován na Enterprise edici." + }, + { + "id": "api.upgrade_to_enterprise.system_not_supported.app_error", + "translation": "Nepodařilo se aktualizovat Mattermost na Enterprise edici. Toto vylepšení funguje pouze na Linuxových systémech s architekturou x86-64." + }, + { + "id": "api.upgrade_to_enterprise.invalid-user.app_error", + "translation": "Nepodařilo se aktualizovat Mattermost na Enterprise edici. Systémový uživatel {{.MattermostUsername}} nemá oprávnění zápisu do patřičného binárního souboru. Systémový administrátor může upravit oprávnění k souboru spuštěním následujícícho příkazu na serveru, kde je Mattermost instalován:\n\n```\nchown {{.MattermostUsername}} \"{{.Path}}\"\n```\n\nPo změně oprávnění se pokuste provést aktualizaci Mattermostu ještě jednou. Poté, co bude aktualizace dokončena a server restartován, nezapomeňte vrátit oprávnění k souboru na původní hodnotu:\n\n```\nchown {{.FileUsername}} \"{{.Path}}\"\n```" + }, + { + "id": "api.upgrade_to_enterprise.invalid-user-and-permission.app_error", + "translation": "Nepodařilo se aktualizovat Mattermost na Enterprise edici. Systémový uživatel {{.MattermostUsername}} nemá oprávnění zápisu do patřičného binárního souboru. Systémový administrátor může upravit oprávnění k souboru spuštěním následujícícho příkazu na serveru, kde je Mattermost instalován:\n\n```\nchown {{.MattermostUsername}} \"{{.Path}}\"\nchmod +w \"{{.Path}}\"\n```\n\nPo změně oprávnění se pokuste provést aktualizaci Mattermostu ještě jednou. Poté, co bude aktualizace dokončena a server restartován, nezapomeňte vrátit oprávnění k souboru na původní hodnotu:\n\n```\nchown {{.FileUsername}} \"{{.Path}}\"\nchmod -w \"{{.Path}}\"\n```" + }, + { + "id": "api.upgrade_to_enterprise.invalid-permission.app_error", + "translation": "Nepodařilo se aktualizovat Mattermost na Enterprise edici. Systémový uživatel {{.MattermostUsername}} nemá oprávnění zápisu do patřičného binárního souboru. Systémový administrátor může upravit oprávnění k souboru spuštěním následujícícho příkazu na serveru, kde je Mattermost instalován:\n\n```\nchmod +w \"{{.Path}}\"\n```\n\nPo změně oprávnění se pokuste provést aktualizaci Mattermostu ještě jednou. Poté, co bude aktualizace dokončena a server restartován, nezapomeňte vrátit oprávnění k souboru na původní hodnotu:\n\n```\nchmod -w \"{{.Path}}\"\n```" + }, + { + "id": "api.upgrade_to_enterprise.generic_error.app_error", + "translation": "Mattermost nemohl bý aktualizován na Enterprise edici." + }, + { + "id": "api.upgrade_to_enterprise.app_error", + "translation": "Aktualizace na Mattermost Enterprise edici je již v běhu." + }, + { + "id": "api.upgrade_to_enterprise.already-enterprise.app_error", + "translation": "Nelze provést aktualizaci, protože již využíváte Mattermost Enterprise edici." + }, + { + "id": "api.upgrade_to_enterprise.already-done.app_error", + "translation": "Již jste aktualizovali na Mattermost Enterprise edici. Prosím restartujte server k dokončení aktualizace." + }, + { + "id": "api.templates.warn_metric_ack.subject", + "translation": "Požadavek z Kontaktuje nás" + }, + { + "id": "api.templates.warn_metric_ack.body.diagnostic_id_header", + "translation": "Id diagnostiky: " + }, + { + "id": "api.team.add_team_member.invalid_body.app_error", + "translation": "Selhalo zprasování těla požadavku." + }, + { + "id": "api.system.update_viewed_notices.failed", + "translation": "Aktualizace zobrazených oznámení selhala" + }, + { + "id": "api.system.update_notices.validating_failed", + "translation": "Validace podmínek produktových oznámení selhalo" + }, + { + "id": "api.system.update_notices.parse_failed", + "translation": "Zpracování produktových oznámení selhalo" + }, + { + "id": "api.system.update_notices.fetch_failed", + "translation": "Získání produktových oznámení selhalo" + }, + { + "id": "api.system.update_notices.clear_failed", + "translation": "Vyčištění starých oznámení selhalo" + }, + { + "id": "api.server.warn_metric.number_of_teams_5.start_trial_notification_success.message", + "translation": "Váš Enterprise test je nyní aktivní. Jděte do **Systémová konzole > Správa uživatelů > Oprávnění ** pro povolení Pokročilých oprávnění." + }, + { + "id": "api.server.warn_metric.number_of_teams_5.start_trial.notification_body", + "translation": "Váš Mattermost má nyní více týmů. Mnoho týmů má své vlastní preference pokud jde o koordinaci a spolupráci, včetně toho, jak jsou zakládány kanály, kdo může pozvat nové členy nebo jak se spravují integrace. Týmová přetížená schémata (Override Schemes) umožňují přizpůsobit oprávnění uživateů pro každý tým zvlášť.\n\n[Zjistit více o Advanced Permissions](https://www.mattermost.com/docs-advanced-permissions-team-override/?utm_medium=product&utm_source=mattermost-advisor-bot&utm_content=advanced-permissions-team-override).\n\nKliknutím na Zahájit test souhlasíte se [Smlouvou o používání programu Mattermost ](https://mattermost.com/software-evaluation-agreement/), [Nastavením soukromí](https://mattermost.com/privacy-policy/) a zasíláním obchodních sdělení." + }, + { + "id": "api.server.warn_metric.number_of_teams_5.notification_title", + "translation": "Použití Advanced Permissions" + }, + { + "id": "api.server.warn_metric.number_of_teams_5.notification_body", + "translation": "Váš Mattermost má nyní více týmů. Mnoho týmů má své vlastní preference pokud jde o koordinaci a spolupráci, včetně toho, jak jsou zakládány kanály, kdo může pozvat nové členy nebo jak se spravují integrace. Týmová přetížená schémata (Override Schemes) umožňují přizpůsobit oprávnění uživateů pro každý tým zvlášť.\n\n[Zjistit více o Advanced Permissions](https://www.mattermost.com/docs-advanced-permissions-team-override/?utm_medium=product&utm_source=mattermost-advisor-bot&utm_content=advanced-permissions-team-override).\n\nKliknutím na Kontaktujte nás budete sdílet své informace s Mattermost, Inc. [Zjistit více](https://mattermost.com/pl/default-admin-advisory)" + }, + { + "id": "api.server.warn_metric.number_of_posts_2M.start_trial.notification_body", + "translation": "Váš Mattermost má velký počet zpráv. Výchozí databázové hledání začíná mít výkonostní problémy kolem 2.5 mil příspěvků. Pří více jak 5 mil příspěvcích může Elasticsearch výrazně pomoci s výkonem. Kontaktujte nás, chcete-li se dozvědět více jak můžeme pomoci.\n\n[Zjistit více o zvýšení výkonu](https://www.mattermost.com/docs-elasticsearch/?utm_medium=product&utm_source=mattermost-advisor-bot&utm_content=elasticsearch)\n\nKliknutím na Zahájit test souhlasíte s [Mattermost Software Evaluation Agreement](https://mattermost.com/software-evaluation-agreement/), [Privacy Policy](https://mattermost.com/privacy-policy/) a příjmem produktových emailů." + }, + { + "id": "api.server.warn_metric.number_of_posts_2M.notification_body", + "translation": "Váš Mattermost má velký počet zpráv. Výchozí databázové hledání začíná mít výkonostní problémy kolem 2.5 mil příspěvků. Pří více jak 5 mil příspěvcích může Elasticsearch výrazně pomoci s výkonem. Kontaktujte nás, chcete-li se dozvědět více jak můžeme pomoci.\n\n[Zjistit více o zvýšení výkonu](https://www.mattermost.com/docs-elasticsearch/?utm_medium=product&utm_source=mattermost-advisor-bot&utm_content=elasticsearch)\n\nKliknutím na Kontaktujte nás budete sdílet své informace s Mattermost, Inc. [Zjistit více](https://mattermost.com/pl/default-admin-advisory)" + }, + { + "id": "api.server.warn_metric.number_of_channels_50.notification_body", + "translation": "Kanály umožňují zlepšit komunikaci, ale s tím, jak je uživatelé napříč Mattermostem zakládají a vstupují do nich, rostou nároky na to udržovat systém organizovaný. Advanced Permissions umožní nastavit, kteří uživatelé nebo role mohou provádět určité akce, jako spravovat nastavení kanálů a jejich členů, využívat @channel nebo @here zmínek, či vytváření nových webhooků.\n\n[Zjistit více o Advanced Permissions](https://www.mattermost.com/docs-advanced-permissions/?utm_medium=product&utm_source=mattermost-advisor-bot&utm_content=advanced-permissions)\n\nKliknutím na Kontaktujte nás budete sdílet své informace s Mattermost, Inc. [Zjistit více](https://mattermost.com/pl/default-admin-advisory)" + }, + { + "id": "model.config.is_valid.import.retention_days_too_low.app_error", + "translation": "Neplatná hodnota pro RetentionDays. Hodnota je příliš nízká." + }, + { + "id": "model.config.is_valid.import.directory.app_error", + "translation": "Neplatná hodnota pro Directory." + }, + { + "id": "model.config.is_valid.collapsed_threads.app_error", + "translation": "Nastavení CollapsedThreads musí být buď disabled,default_on nebo default_off" + }, + { + "id": "import_process.worker.do_job.open_file", + "translation": "Nepodařilo se zpracovat import: selhalo otevření souboru." + }, + { + "id": "import_process.worker.do_job.missing_jsonl", + "translation": "Nepodařilo se zpracovat import: JSONL soubor chybí." + }, + { + "id": "import_process.worker.do_job.missing_file", + "translation": "Nepodařilo se zpracovat import: parametr import_file chybí." + }, + { + "id": "import_process.worker.do_job.file_exists", + "translation": "Nepodařilo se zpracovat import: soubor neexistuje." + }, + { + "id": "app.user.verify_email.app_error", + "translation": "Nepodařilo se aktualizovat pole pro verifikační e-mail." + }, + { + "id": "app.user.update_update.app_error", + "translation": "Nepodařilo se aktualizovat datum poslední změny uživatele." + }, + { + "id": "app.user.update_threads_read_for_user.app_error", + "translation": "Nepodařilo se aktualizovat všechny uživatelova vlákna jako přečtená" + }, + { + "id": "app.user.update_thread_read_for_user.app_error", + "translation": "Nepodařilo se aktualizovat stav o přečtení pro vlákno" + }, + { + "id": "app.user.update_thread_follow_for_user.app_error", + "translation": "Nepodařilo se aktualizovat stav sledování pro vlákno" + }, + { + "id": "app.user.update_failed_pwd_attempts.app_error", + "translation": "Selhala aktualizace údaje failed_attempts." + }, + { + "id": "app.user.update_auth_data.email_exists.app_error", + "translation": "Nepodařilo se přepnout účet na {{.Service}}. Účet používající e-mail {{.Email}} již existuje." + }, + { + "id": "app.user.update_auth_data.app_error", + "translation": "Nepodařilo se aktualizovat autentizační data." + }, + { + "id": "app.user.update_active_for_multiple_users.updating.app_error", + "translation": "Nepodařilo se deaktivovat návštěvníky." + }, + { + "id": "app.user.update.find.app_error", + "translation": "Nepodařilo se najít stávající účet určený k aktualizaci." + }, + { + "id": "app.user.search.app_error", + "translation": "Nepodařilo se nalézt uživatele odpovídající daným kritériím." + }, + { + "id": "app.user.save.username_exists.app_error", + "translation": "Účet se stejným uživatelským jménem již existuje." + }, + { + "id": "app.user.save.existing.app_error", + "translation": "Je nutné použít aktualizaci na existujícího uživatele." + }, + { + "id": "app.user.save.email_exists.app_error", + "translation": "Účet se stejným e-mailem již existuje." + }, + { + "id": "app.user.save.app_error", + "translation": "Selhalo uložení účtu." + }, + { + "id": "app.user.permanent_delete.app_error", + "translation": "Nepodařilo se smazat daný účet." + }, + { + "id": "app.user.missing_account.const", + "translation": "Nepodařilo se najít uživatele." + }, + { + "id": "app.user.get_users_batch_for_indexing.get_users.app_error", + "translation": "Nepodařilo se získat dávku uživatelů k indexaci." + }, + { + "id": "app.user.get_unread_count.app_error", + "translation": "Nepodařilo se získat počet nepřečtených zpráv daného uživatele." + }, + { + "id": "app.user.get_total_users_count.app_error", + "translation": "Nelze spočítat uživatele." + }, + { + "id": "app.user.get_threads_for_user.app_error", + "translation": "Nelze získat vlákna uživatele" + }, + { + "id": "app.user.get_recently_active_users.app_error", + "translation": "Vyskytla se chyba při zjišťování naposledy aktivních uživatelů." + }, + { + "id": "app.user.get_profiles.app_error", + "translation": "Vyskytla se chyba při zjišťování profilů uživatelů." + }, + { + "id": "app.user.get_profile_by_group_channel_ids_for_user.app_error", + "translation": "Vyskytla se chyba při zjišťování profilů uživatelů." + }, + { + "id": "app.user.get_new_users.app_error", + "translation": "Při zjišťování nových uživatelů se vyskytla chyba." + }, + { + "id": "app.user.get_known_users.get_users.app_error", + "translation": "Nelze získat seznam uživatelů z databáze." + }, + { + "id": "app.user.get_by_username.app_error", + "translation": "Nelze najít existující účet, který by odpovídal uživatelskému jménu v tomto týmu. Je možné, že tým vyžaduje pozvánku od vlastníka týmu." + }, + { + "id": "app.user.get_by_auth.other.app_error", + "translation": "Vyskytla se chyba při pokusu najít účet podle typu autentizace." + }, + { + "id": "app.user.get_by_auth.missing_account.app_error", + "translation": "Nelze najít existující účet, který by odpovídal typu autentizace pro tento tým. Je možné, že tým vyžaduje pozvánku od vlastníka týmu." + }, + { + "id": "app.user.get.app_error", + "translation": "Vyskytla se chyba při hledání účtu." + }, + { + "id": "app.user.update.finding.app_error", + "translation": "Vyskytla se chyba při hledání účtu." + }, + { + "id": "app.user.promote_guest.user_update.app_error", + "translation": "Selhala aktualizace uživatele." + }, + { + "id": "app.user.demote_user_to_guest.user_update.app_error", + "translation": "Selhala aktualizace uživatele." + }, + { + "id": "app.user.clear_all_custom_role_assignments.select.app_error", + "translation": "Selhalo získání uživatelů." + }, + { + "id": "app.user.analytics_get_inactive_users_count.app_error", + "translation": "Nemůžeme spočítat neaktivní uživatele." + }, + { + "id": "app.user.analytics_daily_active_users.app_error", + "translation": "Nelze získat aktivní uživatele pro zadané období." + }, + { + "id": "app.upload.upload_data.move_file.app_error", + "translation": "Selhal přesun nahraného souboru." + }, + { + "id": "app.update_error", + "translation": "chyba při update" + }, + { + "id": "app.select_error", + "translation": "chyba při select" + }, + { + "id": "app.insert_error", + "translation": "chyba při vložení" + }, + { + "id": "app.group.uniqueness_error", + "translation": "člen skupiny již existuje" + }, + { + "id": "app.group.permanent_delete_members_by_user.app_error", + "translation": "Nelze odstranit člena skupiny s UserID \"{{.UserId}}\"." + }, + { + "id": "app.group.no_rows", + "translation": "nebyla nalezena žádná opdovídající skupina" + }, + { + "id": "app.group.id.app_error", + "translation": "nevalidní položka Id pro skupinu." + }, + { + "id": "app.group.group_syncable_already_deleted", + "translation": "skupinové syncable již bylo smazáno" + }, + { + "id": "api.templates.payment_failed_no_card.title", + "translation": "Faktura za Mattermost Cloud je v prodlení" + }, + { + "id": "api.templates.payment_failed_no_card.subject", + "translation": "Platba předplatného Mattermost Cloud je ve zpoždění" + }, + { + "id": "api.templates.payment_failed_no_card.info3", + "translation": "Pro kontrolu faktury a zadání platební metody zvolte Zaplatit nyní." + }, + { + "id": "api.templates.payment_failed_no_card.info1", + "translation": "Faktura za Mattermost Cloud byla zpracována. Nicméně nám chybí Vaše platební údaje." + }, + { + "id": "api.templates.payment_failed_no_card.button", + "translation": "Zaplatit nyní" + }, + { + "id": "api.templates.payment_failed.title", + "translation": "Platba neprošla" + }, + { + "id": "api.templates.payment_failed.subject", + "translation": "Vyžadována akce: selhala platba za Mattermost Cloud" + }, + { + "id": "api.templates.payment_failed.info3", + "translation": "Chcete-li nadále nepřerušeně využívat Mattermost Cloud, prosím kontaktujte buď svoji bankovní instituci nebo aktualizujte své platební údaje. Poté, co budou údaje o platbě opraveny, Mattermost se pokusí opětovně provést stržení zbývajících částek." + }, + { + "id": "api.templates.payment_failed.info2", + "translation": "Poskytli následující důvod:" + }, + { + "id": "api.templates.payment_failed.info1", + "translation": "Vaše bankovní instituce zamítla platbu přes {{.CardBrand}} ****{{.LastFour}} připojené k Vašemu Mattermost Cloud workspace." + }, + { + "id": "api.templates.email_us_anytime_at", + "translation": "Napište nám kdykoliv na " + }, + { + "id": "api.templates.copyright", + "translation": "© 2020 Mattermost, Inc. 855 El Camino Real, 13A-168, Palo Alto, CA, 94301" + }, + { + "id": "app.command_webhook.handle_command_webhook.parse", + "translation": "Neporozuměl jsem příchozím datům." + }, + { + "id": "app.command_webhook.try_use.invalid", + "translation": "Nevalidní webhook." + }, + { + "id": "app.command.getcommand.internal_error", + "translation": "Nepodařilo se získat příkaz." + }, + { + "id": "app.channel.get.existing.app_error", + "translation": "Nelze najít daný kanál." + }, + { + "id": "app.channel.get_by_name.existing.app_error", + "translation": "Nelze najít daný kanál." + }, + { + "id": "api.server.warn_metric.number_of_active_users_100.notification_body", + "translation": "Váš Mattermost má přes 100 uživatelů. S tím, jak roste uživatelská základna, správa nových účtů se stává časově nákladnou. Doporučujeme integrovat vaši podnikovou databázi AD/LDAP, která umožní každému účtu přístup k Mattermostu.\n\n[Zjistit více o integraci ADL/LDAP](https://www.mattermost.com/docs-adldap/?utm_medium=product&utm_source=mattermost-advisor-bot&utm_content=adldap)\n\nKliknutím na Kontaktujte nás budete sdílet své informace s Mattermost, Inc. [Zjistit více](https://mattermost.com/pl/default-admin-advisory)" + }, + { + "id": "api.server.warn_metric.number_of_active_users_100.contact_us.email_body", + "translation": "Dotaz na Mattermost. Můj tým má nyní 100 uživatelů a zvažuji využití Mattermost Enterprise Edice.\n" + }, + { + "id": "api.server.warn_metric.mfa.start_trial_notification_success.message", + "translation": "Váš Enterprise test je nyní aktivní. Jděte do **Systémová konzole > Autentizace > VFA** chcete-li vynutit vícefaktorovou autentizaci." + }, + { + "id": "api.server.warn_metric.mfa.start_trial.notification_body", + "translation": "Váš Mattermost má povolenu vícefaktorovou autentizaci, což umožňuje uživatelům zabezpečit jejich účty dalším způsobem, nejen heslem. Ke zvýšení zabezpečení napříč systémem můžete vynutit použivání vícefaktorové autentizace u všech účtů.\n\n[Zjistit více o vynucení vícefaktorové autentizace](https://www.mattermost.com/docs-multi-factor-authentication/?utm_medium=product&utm_source=mattermost-advisor-bot&utm_content=multi-factor-authentication). \n\nKliknutím na Zahájit test souhlasíte s [Mattermost Software Evaluation Agreement](https://mattermost.com/software-evaluation-agreement/), [Privacy Policy](https://mattermost.com/privacy-policy/) a příjmem produktových emailů." + }, + { + "id": "api.server.warn_metric.mfa.notification_title", + "translation": "Vynucení vícefaktorové autentizace" + }, + { + "id": "api.server.warn_metric.mfa.notification_body", + "translation": "Váš Mattermost má povolenu vícefaktorovou autentizaci, což umožňuje uživatelům zabezpečit jejich účty dalším způsobem, nejen heslem. Ke zvýšení zabezpečení napříč systémem můžete vynutit použivání vícefaktorové autentizace u všech účtů.\n\n[Zjistit více o vynucení vícefaktorové autentizace](https://www.mattermost.com/docs-multi-factor-authentication/?utm_medium=product&utm_source=mattermost-advisor-bot&utm_content=multi-factor-authentication). \n\nKliknutím na Kontaktujte nás budete sdílete své informace s Mattermost, Inc. [Zjistit více](https://mattermost.com/pl/default-admin-advisory)" + }, + { + "id": "api.server.warn_metric.mfa.contact_us.email_body", + "translation": "Dotaz na Mattermost. Zajímá mě více o vynucení vícefaktorové autentizace.\n" + }, + { + "id": "api.server.warn_metric.email_domain.start_trial.notification_body", + "translation": "Projekty často zahrnují lidi jak z vnitřku, tak vnějšku organizace. S pomocí návštěvnických účtů můžete zapojit externí partnery do vašeho Mattermostu a určit, s kým mohou pracovat a co mohou vidět.\n\n[Zjistit více o návštěvnických účtech]((https://www.mattermost.com/docs-guest-accounts/?utm_medium=product&utm_source=mattermost-advisor-bot&utm_content=guest-accounts).\n\nKliknutím na Zahájit test souhlasíte s [Mattermost Software Evaluation Agreement](https://mattermost.com/software-evaluation-agreement/), [Privacy Policy](https://mattermost.com/privacy-policy/) a příjmem produktových emailů." + }, + { + "id": "api.server.warn_metric.email_domain.notification_title", + "translation": "Vytvoření návštěvnických účtů" + }, + { + "id": "api.server.warn_metric.email_domain.contact_us.email_body", + "translation": "Dotaz na Mattermost. Mám zájem zjistit více o návštěvnických účtech.\n" + }, + { + "id": "api.server.warn_metric.bot_response.start_trial_failure.message", + "translation": "Testovací licenci nelze stáhnout. Navštivte https://mattermost.com/trial/ kde ji lze získat." + }, + { + "id": "api.roles.patch_roles.not_allowed_permission.error", + "translation": "Jedna nebo více změn v oprávněních, které se pokušíte nastavit, není povolena" + }, + { + "id": "api.post.error_get_post_id.pending", + "translation": "Nelze získat detail čekající zprávy." + }, + { + "id": "api.license.request_trial_license.fail_get_user_count.app_error", + "translation": "Nelze získat testovací licenci, prosím zkuste znovu nebo kontaktujte podporu na support@mattermost.com. Nelze získat počet registrovaných uživatelů." + }, + { + "id": "api.config.patch_config.restricted_merge.app_error", + "translation": "Selhalo sloučení dané konfigurace." + }, + { + "id": "api.server.warn_metric.number_of_teams_5.contact_us.email_body", + "translation": "Dotaz na Mattermost. Chci se dozvědět více o Pokročilých oprávněních s Týmovými schématy.\n" + }, + { + "id": "api.server.warn_metric.number_of_posts_2M.start_trial.notification_success.message", + "translation": "Váš Enterprise test je nyní aktivní. Až budete mít připraven Elasticsearch server, přejděte do **Systémová konzole > Prostředí > Elasticsearch ** pro nastavení Elasticsearch." + }, + { + "id": "api.server.warn_metric.number_of_posts_2M.notification_title", + "translation": "Zlepšení výkonnosti" + }, + { + "id": "api.server.warn_metric.number_of_posts_2M.contact_us.email_body", + "translation": "Dotaz na Mattermost. Zajímalo by mě více o zlepšení výkonnosti s Elasticsearch.\n" + }, + { + "id": "api.server.warn_metric.number_of_channels_50.start_trial.notification_success.message", + "translation": "Váš Enterprise test je nyní aktivní. Jděte do **Systémová konzole > Správa uživatelů > Oprávnění ** pro povolení Pokročilých oprávnění." + }, + { + "id": "api.server.warn_metric.number_of_channels_50.start_trial.notification_body", + "translation": "Kanály mohou zlepšit komunikaci, ale s množstvím uživatelů Mattermostu připojujících se a vytvářejících kanály rostou nároky na udržení přehlednosti systému. Pokročilá oprávnění vám umožní nastavit, kteří uživatelé mohou provádět vybrané akce, např. změnit nastavení, spravovat členy kanálu, používat @channel nebo @here pro označení většího počtu uživatelů či vytvářet nové webhooky.\n\n[Dozvědět se více o Pokročilých oprávněních](https://www.mattermost.com/docs-advanced-permissions/?utm_medium=product&utm_source=mattermost-advisor-bot&utm_content=advanced-permissions)\n\nKliknutím na Zahájit test souhlasíte se [Smlouvou o používání programu Mattermost ](https://mattermost.com/software-evaluation-agreement/), [Nastavením soukromí](https://mattermost.com/privacy-policy/) a zasíláním obchodních sdělení." + }, + { + "id": "app.oauth.update_app.updating.app_error", + "translation": "Při aktualizaci aplikace došlo k chybě." + }, + { + "id": "app.oauth.update_app.find.app_error", + "translation": "Nepodařilo se najít aplikaci k aktualizaci." + }, + { + "id": "app.oauth.save_app.existing.app_error", + "translation": "Je třeba aktualizovat stávající aplikaci." + }, + { + "id": "app.oauth.remove_access_data.app_error", + "translation": "Nelze odstranit přístupový token." + }, + { + "id": "app.oauth.permanent_delete_auth_data_by_user.app_error", + "translation": "Nelze odstranit autorizační kód." + }, + { + "id": "app.oauth.get_apps.find.app_error", + "translation": "Chyba při hledání OAuth2 aplikací." + }, + { + "id": "app.oauth.get_app_by_user.find.app_error", + "translation": "Nelze najít žádné existující aplikace." + }, + { + "id": "app.oauth.get_app.finding.app_error", + "translation": "Chyba při hledání aplikace." + }, + { + "id": "app.oauth.get_app.find.app_error", + "translation": "Nelze najít požadovanou aplikaci." + }, + { + "id": "app.oauth.get_access_data_by_user_for_app.app_error", + "translation": "Chyba při hledání všech přístupových tokenů." + }, + { + "id": "app.oauth.delete_app.app_error", + "translation": "Chyba při odstranění OAuth2 aplikace." + }, + { + "id": "app.job.update.app_error", + "translation": "Nelze upravit úkol." + }, + { + "id": "app.job.save.app_error", + "translation": "Nelze uložit úkol." + }, + { + "id": "app.job.get_newest_job_by_status_and_type.app_error", + "translation": "Nelze načíst poslední úkol pro daný status a typ." + }, + { + "id": "app.job.get_count_by_status_and_type.app_error", + "translation": "Nelze načíst počet úkolů pro daný status a typ." + }, + { + "id": "app.job.get_all.app_error", + "translation": "Nelze načíst úkoly." + }, + { + "id": "app.job.get.app_error", + "translation": "Nelze načíst úkol." + }, + { + "id": "app.job.download_export_results_not_enabled", + "translation": "DownloadExportResults v config.json nastaveno na false. Nastavte na true pro stažení výsledků úkolu." + }, + { + "id": "app.import.validate_user_import_data.auth_data_and_service_dependency.error", + "translation": "Musí být použity obě položky uživatele AuthService a AuthData zároveň." + }, + { + "id": "app.import.validate_post_import_data.props_too_large.error", + "translation": "Položka Props v požadavku je delší než je maximální povolená délka." + }, + { + "id": "app.import.import_user_teams.save_members.max_accounts.app_error", + "translation": "Nepodařilo se naimportovat členství v týmu, protože nelze přidat dalšího člena do daného týmu" + }, + { + "id": "app.import.import_user_teams.save_members.error", + "translation": "Nepodařilo se naimportovat členství v týmu" + }, + { + "id": "app.import.import_user_teams.save_members.conflict.app_error", + "translation": "Nepodařilo se naimportovat nové členství v týmu, protože již existuje" + }, + { + "id": "app.import.import_user_channels.channel_not_found.error", + "translation": "Nepodařilo se naimportovat kanály uživatele. Kanál nenalezen." + }, + { + "id": "app.file_info.save.app_error", + "translation": "Nelze uložit soubor s informacemi." + }, + { + "id": "app.file_info.permanent_delete_by_user.app_error", + "translation": "Nelze smazat přílohy daného uživatele." + }, + { + "id": "app.file_info.get_with_options.app_error", + "translation": "Nelze načíst soubor s informacemi o nastavení" + }, + { + "id": "app.file_info.get.app_error", + "translation": "Nelze načíst soubor s informacemi." + }, + { + "id": "app.file_info.get_for_post.app_error", + "translation": "Nelze načíst soubor s informacemi k danému příspěvku." + }, + { + "id": "app.email.setup_rate_limiter.app_error", + "translation": "Došlo k chybě v omezovači toku." + }, + { + "id": "app.email.rate_limit_exceeded.app_error", + "translation": "Překročen počet e-mailových pozvánek. Časové omezení vyprší za {{.ResetAfter}} vteřin. Zkuste znovu za {{.RetryAfter}} vteřin." + }, + { + "id": "app.email.no_rate_limiter.app_error", + "translation": "Omezovač toku není nastaven." + }, + { + "id": "app.create_basic_user.save_member.max_accounts.app_error", + "translation": "Nelze vytvořit výchozí členství v týmech, nelze přidat dalšího člena do daného týmu" + }, + { + "id": "app.create_basic_user.save_member.conflict.app_error", + "translation": "Nelze vytvořit výchozí členství v týmech, protože již existují" + }, + { + "id": "app.create_basic_user.save_member.app_error", + "translation": "Nelze vytvořit výchozí členství v týmech" + }, + { + "id": "app.command_webhook.create_command_webhook.existing", + "translation": "Nemůžete upravovat existující CommandWebhook." + }, + { + "id": "app.command.tryexecutecustomcommand.internal_error", + "translation": "Nelze provést vlastní příkaz." + }, + { + "id": "app.command.regencommandtoken.internal_error", + "translation": "Nelze obnovit token pro příkaz." + }, + { + "id": "app.command.listteamcommands.internal_error", + "translation": "Nelze získat seznam týmových příkazů." + }, + { + "id": "app.command.listautocompletecommands.internal_error", + "translation": "Nelze získat seznam autocomplete příkazů." + }, + { + "id": "app.channel_member_history.log_leave_event.internal_error", + "translation": "Selhal záznam historie uživatelů kanálu. Selhal update existujícího záznamu" + }, + { + "id": "app.channel_member_history.log_join_event.internal_error", + "translation": "Selhal záznam historie uživatelů kanálu." + }, + { + "id": "app.channel.user_belongs_to_channels.app_error", + "translation": "Nepodařilo se zjistit, zda daný uživatel patří do některého kanálu." + }, + { + "id": "app.channel.update_last_viewed_at_post.app_error", + "translation": "Nelze označit kanál jako nepřečtený." + }, + { + "id": "app.channel.sidebar_categories.app_error", + "translation": "Selhalo vložení záznamu do databáze." + }, + { + "id": "app.channel.search_group_channels.app_error", + "translation": "Nepodařilo se sloučit kanály pro daného uživatele a výraz." + }, + { + "id": "app.channel.search.app_error", + "translation": "Při hledání kanálů došlo k chybě." + }, + { + "id": "app.channel.save_member.exists.app_error", + "translation": "Uživatel kanálu s daným ID již existuje." + }, + { + "id": "app.channel.reset_all_channel_schemes.app_error", + "translation": "Nepodařil se reset schémat kanálu." + }, + { + "id": "app.channel.remove_member.app_error", + "translation": "Nepodařilo se odebrat člena kanálu." + }, + { + "id": "app.channel.remove_all_deactivated_members.app_error", + "translation": "Nepodařilo se odebrat deaktivované členy kanálu." + }, + { + "id": "app.channel.pinned_posts.app_error", + "translation": "Nepodařilo se najít připnuté příspěvky." + }, + { + "id": "app.channel.permanent_delete_members_by_user.app_error", + "translation": "Nepodařilo se odebrat člena kanálu." + }, + { + "id": "app.channel.get_all_channels_count.app_error", + "translation": "Nepodařilo se zjistit počet kanálů." + }, + { + "id": "app.channel.migrate_channel_members.select.app_error", + "translation": "Selhal hromadný výběr členů kanálu." + }, + { + "id": "app.channel.get_unread.app_error", + "translation": "Nepodařilo se zjistit počet nepřečtených zpráv v kanálu." + }, + { + "id": "app.channel.get_public_channels.get.app_error", + "translation": "Nepodařilo se načíst veřejné kanály." + }, + { + "id": "app.channel.get_private_channels.get.app_error", + "translation": "Nepodařilo se načíst soukromé kanály." + }, + { + "id": "app.channel.get_pinnedpost_count.app_error", + "translation": "Nepodařilo se načíst počet připnutých příspěvků v kanálu." + }, + { + "id": "app.channel.get_members_by_ids.app_error", + "translation": "Nepodařilo se načíst členy kanálu." + }, + { + "id": "app.channel.get_members.app_error", + "translation": "Nepodařilo se načíst členy kanálu." + }, + { + "id": "app.channel.get_member_count.app_error", + "translation": "Nepodařilo se zjistit počet členů kanálu." + }, + { + "id": "app.channel.get_member.missing.app_error", + "translation": "Nepodařilo se najít kanál pro dané ID uživatele a ID kanálu." + }, + { + "id": "app.channel.get_member.app_error", + "translation": "Nepodařilo se získat člena kanálu." + }, + { + "id": "app.channel.get_for_post.app_error", + "translation": "Nepodařilo se získat kanál pro daný příspěvek." + }, + { + "id": "app.channel.get_channels_by_ids.not_found.app_error", + "translation": "Nenalezen žádný kanál." + }, + { + "id": "app.channel.get_channels_by_ids.app_error", + "translation": "Nepodařilo se získat kanály pro daná id." + }, + { + "id": "app.channel.get_channels_batch_for_indexing.get.app_error", + "translation": "Nepodařilo se získat dávku kanálů pro indexaci." + }, + { + "id": "app.channel.get_channel_counts.get.app_error", + "translation": "Nepodařilo se získat počet kanálů." + }, + { + "id": "app.channel.get_by_scheme.app_error", + "translation": "Nepodařilo se získat kanály pro dané schéma." + }, + { + "id": "app.channel.get_all_direct.app_error", + "translation": "Nepodařilo se získat všechny přímé kanály." + }, + { + "id": "app.channel.get_all_channels.app_error", + "translation": "Nepodařilo se získat všechny kanály." + }, + { + "id": "app.channel.get_all.app_error", + "translation": "Nepodařilo se získat všechny kanály." + }, + { + "id": "app.channel.get.find.app_error", + "translation": "Při hledání kanálu došlo k chybě." + }, + { + "id": "app.channel.create_initial_sidebar_categories.internal_error", + "translation": "Nepodařilo se vytvořit úvodní panel kategorií pro uživatele." + }, + { + "id": "app.post.get_flagged_posts.app_error", + "translation": "Označené příspěvky nelze získat." + }, + { + "id": "app.post.get_direct_posts.app_error", + "translation": "Zprávy nelze získat." + }, + { + "id": "app.post.get.app_error", + "translation": "Příspěvek nelze načíst." + }, + { + "id": "app.post.delete.app_error", + "translation": "Příspěvek nelze smazat." + }, + { + "id": "app.post.analytics_user_counts_posts_by_day.app_error", + "translation": "Nelze získat počet uživatelů s příspěvky." + }, + { + "id": "app.post.analytics_posts_count_by_day.app_error", + "translation": "Nelze získat počet příspěvků za den." + }, + { + "id": "ent.compliance.global_relay.write_file.appError", + "translation": "Nemohu zapsat soubor global relay." + }, + { + "id": "ent.compliance.csv.write_file.appError", + "translation": "Nemohu zapsat CSV soubor." + }, + { + "id": "ent.actiance.export.write_file.appError", + "translation": "Nemohu zapsat exportní soubor." + }, + { + "id": "app.license.generate_renewal_token.no_license", + "translation": "Chybí licence" + }, + { + "id": "app.license.generate_renewal_token.bad_license", + "translation": "Tento typ licence nepodporuje tvorbu tokenu pro obnovení" + }, + { + "id": "app.license.generate_renewal_token.app_error", + "translation": "Selhalo vytvoření tokenu pro obnovení." + }, + { + "id": "api.license.request_renewal_link.app_error", + "translation": "Chyba při získávání odkazu na obnovení licence" + }, + { + "id": "api.file.write_file.app_error", + "translation": "Soubor nelze zapsat." + }, + { + "id": "api.file.test_connection.app_error", + "translation": "Nelze přistoupit k úložišti souborů." + }, + { + "id": "api.file.remove_file.app_error", + "translation": "Soubor nelze odstranit." + }, + { + "id": "api.file.remove_directory.app_error", + "translation": "Adresář nelze odstranit." + }, + { + "id": "api.file.read_file.app_error", + "translation": "Soubor nelze přečíst." + }, + { + "id": "api.file.move_file.app_error", + "translation": "Nelze přesunout soubor." + }, + { + "id": "api.file.list_directory.app_error", + "translation": "Nelze vylistovat adresář." + }, + { + "id": "api.file.file_size.app_error", + "translation": "Nelze získat velikost souboru." + }, + { + "id": "api.file.file_reader.app_error", + "translation": "Nelze získat prostředek pro čtení souboru." + }, + { + "id": "api.file.file_exists.app_error", + "translation": "Nelze ověřit existenci souboru." + }, + { + "id": "api.file.append_file.app_error", + "translation": "Nelze přidat do souboru data." + }, + { + "id": "app.post.permanent_delete_by_channel.app_error", + "translation": "Nelze smazat příspěvky dle kanálu." + }, + { + "id": "app.post.overwrite.app_error", + "translation": "Nelze přepsat příspěvek." + }, + { + "id": "app.post.get_root_posts.app_error", + "translation": "Nelze načíst příspěvky kanálu." + }, + { + "id": "app.post.get_posts_since.app_error", + "translation": "Nelze načíst příspěvky kanálu." + }, + { + "id": "app.post.get_posts_created_at.app_error", + "translation": "Nelze načíst příspěvky kanálu." + }, + { + "id": "app.post.get_posts_batch_for_indexing.get.app_error", + "translation": "Nelze načíst dávku příspěvků k indexaci." + }, + { + "id": "app.post.get_posts_around.get.app_error", + "translation": "Nelze načíst příspěvky kanálu." + }, + { + "id": "app.post.get_post_id_around.app_error", + "translation": "Nelze načíst příspěvek kolem časového omezení." + }, + { + "id": "app.post.get_post_after_time.app_error", + "translation": "Nelze načíst příspěvek po časovém omezení." + }, + { + "id": "app.post.analytics_posts_count.app_error", + "translation": "Nelze získat počet příspěvků." + }, + { + "id": "app.compliance.get.finding.app_error", + "translation": "Při načítání zprávy o shodě došlo k chybě." + }, + { + "id": "app.compliance.save.saving.app_error", + "translation": "Při ukládání zprávy o shodě došlo k chybě." + }, + { + "id": "app.channel.update_last_viewed_at.app_error", + "translation": "Nepodařilo se aktualizovat naposledy zobrazeno v čase." + }, + { + "id": "app.export.marshal.app_error", + "translation": "Nerozumím odpovědi." + }, + { + "id": "app.export.export_attachment.zip_create_header.error", + "translation": "Během exportu selhalo vytvoření zip hlavičky." + }, + { + "id": "app.export.export_attachment.mkdirall.error", + "translation": "Během exportu selhalo vytvoření adresáře." + }, + { + "id": "app.export.export_attachment.create_file.error", + "translation": "Během exportu selhalo vytvoření souboru." + }, + { + "id": "app.export.export_attachment.copy_file.error", + "translation": "Během exportu selhalo kopírování souboru." + }, + { + "id": "api.user.update_user_auth.invalid_request", + "translation": "Požadavek neobsahuje parametr AuthData nebo AuthService." + }, + { + "id": "api.user.get_authorization_code.endpoint.app_error", + "translation": "Chyba při získávání endpointu z Discovery Document." + }, + { + "id": "api.unable_to_read_file_from_backend", + "translation": "Chyba při čtení souboru ze serveru" + }, + { + "id": "api.unable_to_create_zip_file", + "translation": "Chyba při vytváření souboru zip." + }, + { + "id": "api.oauth.redirecting_back", + "translation": "Přesměrování zpět do aplikace." + }, + { + "id": "api.oauth.close_browser", + "translation": "Nyní můžete zavřít tab prohlížeče." + }, + { + "id": "api.oauth.auth_complete", + "translation": "Autentizace dokončena" + }, + { + "id": "api.no_license", + "translation": "Licence E10 nebo E20 jsou vyžadovány pro tento endpoint." + }, + { + "id": "api.job.unable_to_download_job.incorrect_job_type", + "translation": "Typ úlohy, kterou se snažíte stáhnout, není v tuto chvíli podporován" + }, + { + "id": "api.invalid_redirect_url", + "translation": "Bylo zadáno nevalidní url pro přesměrování" + }, + { + "id": "api.invalid_custom_url_scheme", + "translation": "Bylo zadáno nevalidní url schéma" + }, + { + "id": "api.file.file_mod_time.app_error", + "translation": "Nemohu zjistit čas poslední změny souboru." + }, + { + "id": "api.export.export_not_found.app_error", + "translation": "Nemohu najít soubor s exportem." + }, + { + "id": "api.config.update_config.not_allowed_security.app_error", + "translation": "Změna {{.Name}} není povolena z bezpečnostních důvodů." + }, + { + "id": "api.back_to_app", + "translation": "Zpět na {{.SiteName}}" + }, + { + "id": "api.command_remote.decode_invitation.error", + "translation": "Pozvánka nemohla být dekódována: {{.Error}}" + }, + { + "id": "api.command_remote.cluster_removed", + "translation": "Zabezpečené připojení {{.RemoteId}} {{.Result}}." + }, + { + "id": "api.command_remote.add_remote.error", + "translation": "Zabezpečené spojení nebylo přidáno: {{.Error}}" + }, + { + "id": "api.command_remote.accept_invitation.error", + "translation": "Pozvánka nebyla přijata: {{.Error}}" + }, + { + "id": "api.command_remote.accept.help", + "translation": "Přijmout pozvánku z externí instance Mattermost" + }, + { + "id": "api.command_remote.accept_invitation", + "translation": "Pozvánka přijata a potvrzena.\nSiteURL: {{.SiteURL}}" + }, + { + "id": "api.command_channel_purpose.update_channel.max_length", + "translation": "Vložený text je příliš dlouhý. Délka textu účelu kanálu je omezena na maximálně {{.MaxLength}} znaky/ů." + }, + { + "id": "api.command_channel_header.update_channel.max_length", + "translation": "Vložený text je příliš dlouhý. Délka textu záhlaví kanálu je omezena na maximálně {{.MaxLength}} znaky/ů." + }, + { + "id": "api.cloud.cws_webhook_event_missing_error", + "translation": "Událost z webhooku nebyla zpracována. Chybí, nebo není platná." + }, + { + "id": "api.channel.patch_channel_moderations.cache_invalidation.error", + "translation": "Chyba při zneplatnění vyrovnávací paměti cache" + }, + { + "id": "api.channel.create_channel.direct_channel.team_restricted_error", + "translation": "Kanál přímých zpráv mezi těmito uživateli nemohl být vytvořen, protože nejsou v žádném týmu společně." + }, + { + "id": "api.admin.saml.failure_reset_authdata_to_email.app_error", + "translation": "Nepodařilo se resetovat pole AuthData na Email." + }, + { + "id": "api.cloud.notify_admin_to_upgrade_error.already_notified", + "translation": "Administrátor byl již upozorněn" + }, + { + "id": "Channels", + "translation": "Kanály" + }, + { + "id": "worktemplate.product_teams.feature_release.description.playbook", + "translation": "Vytvořte transparentní pracovní postupy napříč vývojovými týmy, abyste zajistili bezproblémový proces vývoje funkcí." + }, + { + "id": "api.admin.syncables_error", + "translation": "Nepodařilo se přidat uživatele do skupinových týmů a skupinových kanálů" + }, + { + "id": "api.command_remote.fetch_status.error", + "translation": "Nepodařilo se načíst zabezpečená připojení: {{.Error}}" + }, + { + "id": "api.command_remote.encrypt_invitation.error", + "translation": "Pozvánku se nepodařilo zašifrovat: {{.Error}}" + }, + { + "id": "api.command_remote.displayname.hint", + "translation": "Zobrazovaný název zabezpečeného připojení" + }, + { + "id": "api.command_remote.displayname.help", + "translation": "Zobrazovaný název zabezpečeného připojení" + }, + { + "id": "api.command_remote.desc", + "translation": "Pozvat zabezpečené připojení pro komunikaci mezi instancemi Mattermost." + }, + { + "id": "api.command_marketplace.unsupported.app_error", + "translation": "Příkaz marketplace není na vašem zařízení podporován." + }, + { + "id": "api.command_marketplace.name", + "translation": "obchod" + }, + { + "id": "api.command_marketplace.desc", + "translation": "Otevřít obchod" + }, + { + "id": "api.cloud.teams_limit_reached.restore", + "translation": "Tým nelze obnovit, protože bylo dosaženo limitu týmů" + }, + { + "id": "api.cloud.teams_limit_reached.create", + "translation": "Tým nelze vytvořit, protože bylo dosaženo limitu týmů" + }, + { + "id": "api.cloud.delinquency_email.missing_email_to_trigger", + "translation": "Chybějící povinná pole pro odeslání e-mailu o prodlení." + }, + { + "id": "web.error.unsupported_browser.no_longer_support", + "translation": "Tento prohlížeč již Mattermost nepodporuje" + }, + { + "id": "web.incoming_webhook.channel_locked.app_error", + "translation": "Tento webhook nemá povoleno přispívat na požadovaný kanál." + }, + { + "id": "api.acknowledgement.save.archived_channel.app_error", + "translation": "V archivovaném kanálu nemůžete potvrdit potvrzení." + }, + { + "id": "api.acknowledgement.delete.deadline.app_error", + "translation": "Po 5 minutách již potvrzení nelze smazat." + }, + { + "id": "api.acknowledgement.delete.archived_channel.app_error", + "translation": "V archivovaném kanálu nelze odebrat potvrzení." + }, + { + "id": "Playbooks", + "translation": "Scénáře" + }, + { + "id": "Boards", + "translation": "Nástěnky" + }, + { + "id": "api.command_remote.remotes_not_found", + "translation": "Nebyla nalezena žádná zabezpečená připojení." + }, + { + "id": "api.command_remote.remote_table_header", + "translation": "| Zabezpečené připojení | Zobrazované jméno | ID připojení | Adresa URL webu | Přijatá pozvánka | Online | Poslední ping |" + }, + { + "id": "api.command_remote.remote_add_remove.help", + "translation": "Přidání/odstranění zabezpečených připojení. Dostupné akce: {{.Actions}}" + }, + { + "id": "api.command_remote.permission_required", + "translation": "Ke správě zabezpečených připojení potřebujete oprávnění `{{.Permission}}`." + }, + { + "id": "api.command_remote.name.hint", + "translation": "Zobrazovaný název zabezpečeného připojení" + }, + { + "id": "api.command_remote.name.help", + "translation": "Název zabezpečeného připojení" + }, + { + "id": "api.command_remote.name", + "translation": "zabezpečené připojení" + }, + { + "id": "api.command_remote.missing_empty", + "translation": "Chybějící nebo prázdný `{{.Arg}}`" + }, + { + "id": "api.command_remote.missing_command", + "translation": "Chybějící příkaz. Dostupné akce: {{.Actions}}" + }, + { + "id": "api.command_remote.invite_summary", + "translation": "Externímu správci systému Mattermost odešlete následující pozvánku zašifrovanou 256bitovým algoritmem AES spolu s heslem. K přijetí pozvánky použije příkaz `{{.Command}}` s lomítkem.\n\n```\n{{.Invitation}}\n```\n\n**Zajistěte, aby zabezpečené připojení mohlo přistupovat k vaší instanci Mattermostu prostřednictvím** {{.SiteURL}}" + }, + { + "id": "api.command_remote.invite_password.hint", + "translation": "Heslo, které se použije k zašifrování pozvánky" + }, + { + "id": "api.command_remote.invite_password.help", + "translation": "Heslo pozvánky" + }, + { + "id": "api.command_remote.invite.help", + "translation": "Pozvání k zabezpečenému připojení" + }, + { + "id": "api.command_remote.invitation_created", + "translation": "Vytvořená pozvánka." + }, + { + "id": "api.command_remote.invitation.hint", + "translation": "Šifrovaná pozvánka ze zabezpečeného připojení" + }, + { + "id": "api.command_remote.invitation.help", + "translation": "Pozvánka ze zabezpečeného připojení" + }, + { + "id": "api.command_remote.incorrect_password.error", + "translation": "Pozvánku se nepodařilo dešifrovat. Nesprávné heslo nebo poškozená pozvánka: {{.Error}}" + }, + { + "id": "api.command_remote.hint", + "translation": "[akce]" + }, + { + "id": "api.command_help.success", + "translation": "Mattermost je open source platforma pro bezpečnou komunikaci, spolupráci a orchestraci práce napříč nástroji a týmy.\nMattermost obsahuje tři klíčové nástroje:\n\n**Kanály** - Zůstaňte ve spojení se svým týmem prostřednictvím zpráv 1:1 a skupinových zpráv.\n**[Playbooks](/playbooks)** - Vytvářejte a konfigurujte opakovatelné procesy k dosažení konkrétních a předvídatelných výsledků.\n**[Nástěnky](/boards)** - Spravujte projekty a úkoly ve struktuře nástěnky Kanban, která vašemu týmu pomůže dosáhnout klíčových milníků.\n\n[Zobrazit dokumentaci a průvodce]({{.HelpLink}})" + }, + { + "id": "api.command_custom_status.clear.app_error", + "translation": "Chyba při mazání stavu" + }, + { + "id": "api.command_custom_status.app_error", + "translation": "Chyba při nastavování stavu" + } +] diff --git a/server/i18n/da.json b/server/i18n/da.json new file mode 100644 index 00000000000..250ad0aff8d --- /dev/null +++ b/server/i18n/da.json @@ -0,0 +1,446 @@ +[ + { + "id": "interactive_message.generate_trigger_id.signing_failed", + "translation": "Kunne ikke underskrive genereret trigger ID til interaktiv dialog." + }, + { + "id": "interactive_message.decode_trigger_id.verify_signature_failed", + "translation": "Bekræftelse af signatur mislykkedes for trigger ID til interaktiv dialog." + }, + { + "id": "interactive_message.decode_trigger_id.signature_decode_failed", + "translation": "Afkodning af base64 signatur, for trigger ID til interaktiv dialog, mislykkedes." + }, + { + "id": "interactive_message.decode_trigger_id.missing_data", + "translation": "Trigger ID mangler nødvendig data til interaktiv dialog." + }, + { + "id": "interactive_message.decode_trigger_id.expired", + "translation": "Trigger ID tilhørende interaktiv dialog er udløbet. Trigger ID'er lever i maksimalt {{.Seconds}} sekunder." + }, + { + "id": "interactive_message.decode_trigger_id.base64_decode_failed_signature", + "translation": "Afkodning af base64 signatur, for trigger ID til interaktiv dialog, mislykkedes." + }, + { + "id": "interactive_message.decode_trigger_id.base64_decode_failed", + "translation": "Afkodning af base64 for trigger ID til interaktiv dialog mislykkedes." + }, + { + "id": "ent.saml.service_disable.app_error", + "translation": "SAML 2.0 er ikke konfigureret eller understøttet på denne server." + }, + { + "id": "ent.saml.metadata.app_error", + "translation": "Der opstod en fejl under konstruktion af Service Provider Metadata." + }, + { + "id": "app.submit_interactive_dialog.json_error", + "translation": "Der opstod en fejl i kodning JSON for den interaktive dialog." + }, + { + "id": "app.plugin.write_file.saving.app_error", + "translation": "Der opstod en fejl under gem af filen." + }, + { + "id": "app.plugin.write_file.read.app_error", + "translation": "Der opstod en fejl under læsning af filen." + }, + { + "id": "app.plugin.delete_public_key.delete.app_error", + "translation": "Der opstod en fejl under sletningen af offentlig nøgle." + }, + { + "id": "api.user.upload_profile_user.too_large.app_error", + "translation": "Kan ikke uploade profilbillede. Filen er for stor." + }, + { + "id": "api.user.upload_profile_user.storage.app_error", + "translation": "Ude af stand til at uploade billedet. Lagring af billeder er ikke konfigureret." + }, + { + "id": "api.user.upload_profile_user.parse.app_error", + "translation": "Kunne ikke oversætte multipart-form." + }, + { + "id": "api.user.upload_profile_user.open.app_error", + "translation": "Kunne ikke åbne certifikat-fil." + }, + { + "id": "api.user.upload_profile_user.no_file.app_error", + "translation": "Ingen fil under \"billede\" i anmodningen." + }, + { + "id": "api.user.upload_profile_user.array.app_error", + "translation": "Tom tabel under \"billede\" i forespørgsel." + }, + { + "id": "api.user.saml.not_available.app_error", + "translation": "SAML 2.0 er ikke konfigureret eller understøttet på denne server." + }, + { + "id": "api.team.set_team_icon.too_large.app_error", + "translation": "Kan ikke uploade gruppe-ikon. Filen er for stor." + }, + { + "id": "api.team.set_team_icon.storage.app_error", + "translation": "Ude af stand til at uploade billedet. Lagring af billeder er ikke konfigureret." + }, + { + "id": "api.team.set_team_icon.parse.app_error", + "translation": "Kunne ikke oversætte multipart-form." + }, + { + "id": "api.team.set_team_icon.open.app_error", + "translation": "Kunne ikke åbne certifikat-fil." + }, + { + "id": "api.team.set_team_icon.no_file.app_error", + "translation": "Ingen fil under \"billede\" i anmodningen." + }, + { + "id": "api.team.set_team_icon.array.app_error", + "translation": "Tom tabel under \"billede\" i forespørgsel." + }, + { + "id": "api.team.import_team.parse.app_error", + "translation": "Kunne ikke oversætte multipart-form." + }, + { + "id": "api.team.import_team.open.app_error", + "translation": "Kunne ikke åbne certifikat-fil." + }, + { + "id": "api.team.import_team.no_file.app_error", + "translation": "Ingen fil under \"fil\" i anmodningen." + }, + { + "id": "api.team.import_team.array.app_error", + "translation": "Tom tabel under \"billede\" i forespørgsel." + }, + { + "id": "api.team.add_user_to_team.added", + "translation": "%v tilføjet til kanalen af %v." + }, + { + "id": "api.restricted_system_admin", + "translation": "Denne handling er ikke tilladt for en begrænset system admin." + }, + { + "id": "api.license.add_license.open.app_error", + "translation": "Kunne ikke åbne certifikat-fil." + }, + { + "id": "api.license.add_license.no_file.app_error", + "translation": "Ingen fil under \"licens\" i anmodningen." + }, + { + "id": "api.license.add_license.array.app_error", + "translation": "Tom tabel under \"billede\" i forespørgsel." + }, + { + "id": "api.file.upload_file.storage.app_error", + "translation": "Ude af stand til at uploade billedet. Lagring af billeder er ikke konfigureret." + }, + { + "id": "api.command_msg.fail.app_error", + "translation": "Der opstod en fejl under besked til bruger." + }, + { + "id": "api.command_leave.fail.app_error", + "translation": "Der opstod en fejl ved forlad kanalen." + }, + { + "id": "api.command_join.list.app_error", + "translation": "Der opstod en fejl under visning af kanalen." + }, + { + "id": "api.command_join.fail.app_error", + "translation": "Der opstod en fejl under tilslutning til kanalen." + }, + { + "id": "api.command_invite.fail.app_error", + "translation": "Der opstod en fejl under tilslutning til kanalen." + }, + { + "id": "api.command_groupmsg.fail.app_error", + "translation": "Der opstod en fejl mens der blev sendt besked til brugere." + }, + { + "id": "api.command_expand_collapse.fail.app_error", + "translation": "Der opstod en fejl mens forhåndsvisninger blev udvidet." + }, + { + "id": "api.command.invite_people.name", + "translation": "invite_people" + }, + { + "id": "api.channel.remove_user_from_channel.app_error", + "translation": "Kan ikke tilføje bruger til denne type kanal." + }, + { + "id": "api.channel.change_channel_privacy.public_to_private", + "translation": "Denne kanal er blevet omdannet til et Privat Kanal." + }, + { + "id": "api.channel.change_channel_privacy.private_to_public", + "translation": "Denne kanal er blevet konverteret til en offentlig kanal og alle slags gruppe medlemmer kan nu tilslutte sig." + }, + { + "id": "api.channel.add_user_to_channel.type.app_error", + "translation": "Kan ikke tilføje bruger til denne type kanal." + }, + { + "id": "api.channel.add_user.to.channel.failed.deleted.app_error", + "translation": "Kunne ikke tilføje bruger til kanal, fordi de er blevet fjernet fra gruppen." + }, + { + "id": "api.channel.add_user.to.channel.failed.app_error", + "translation": "Kunne ikke tilføje bruger til kanal." + }, + { + "id": "api.channel.add_member.added", + "translation": "%v tilføjet til kanalen af %v." + }, + { + "id": "api.channel.add_guest.added", + "translation": "%v tilføjet til kanalen af %v." + }, + { + "id": "api.admin.upload_brand_image.too_large.app_error", + "translation": "Kan ikke uploade fil. Filen er for stor." + }, + { + "id": "api.admin.upload_brand_image.storage.app_error", + "translation": "Ude af stand til at uploade billedet. Lagring af billeder er ikke konfigureret." + }, + { + "id": "api.admin.upload_brand_image.parse.app_error", + "translation": "Kunne ikke oversætte multipart-form." + }, + { + "id": "api.admin.upload_brand_image.no_file.app_error", + "translation": "Ingen fil under \"billede\" i anmodningen." + }, + { + "id": "api.admin.upload_brand_image.array.app_error", + "translation": "Tom tabel under \"billede\" i forespørgsel." + }, + { + "id": "api.admin.test_s3.missing_s3_bucket", + "translation": "S3 Bucket er påkrævet" + }, + { + "id": "api.admin.test_email.subject", + "translation": "Mattermost - Test af E-Mail indstillinger" + }, + { + "id": "api.admin.test_email.reenter_password", + "translation": "SMTP-server, port, eller brugernavn var ændret. Venligst indtast SMTP-adgangskode for at teste forbindelsen." + }, + { + "id": "api.admin.test_email.missing_server", + "translation": "SMTP Server er påkrævet" + }, + { + "id": "api.admin.test_email.body", + "translation": "Det ser ud til at din Mattermost e-mail er sat korrekt op!" + }, + { + "id": "api.admin.saml.not_available.app_error", + "translation": "SAML 2.0 er ikke konfigureret eller understøttet på denne server." + }, + { + "id": "api.admin.saml.metadata.app_error", + "translation": "Der opstod en fejl under konstruktion af Service Provider Metadata." + }, + { + "id": "api.admin.saml.failure_save_idp_certificate_file.app_error", + "translation": "Kunne ikke gemme certifikat-fil." + }, + { + "id": "api.admin.remove_certificate.delete.app_error", + "translation": "Der opstod en fejl under sletningen af certifikatet." + }, + { + "id": "api.admin.get_brand_image.storage.app_error", + "translation": "Billedlager er ikke konfigureret." + }, + { + "id": "api.admin.file_read_error", + "translation": "Fejl ved læsning af log-fil." + }, + { + "id": "api.admin.delete_brand_image.storage.not_found", + "translation": "Ikke muligt at slette mærke billede, ikke fundet." + }, + { + "id": "api.admin.add_certificate.saving.app_error", + "translation": "Kunne ikke gemme certifikat-fil." + }, + { + "id": "api.admin.add_certificate.open.app_error", + "translation": "Kunne ikke åbne certifikat-fil." + }, + { + "id": "api.admin.add_certificate.no_file.app_error", + "translation": "Ingen fil under \"certifikat\" i anmodningen." + }, + { + "id": "api.admin.add_certificate.array.app_error", + "translation": "Ingen fil under \"certifikat\" i anmodningen." + }, + { + "id": "September", + "translation": "September" + }, + { + "id": "October", + "translation": "Oktober" + }, + { + "id": "November", + "translation": "November" + }, + { + "id": "May", + "translation": "Maj" + }, + { + "id": "March", + "translation": "Marts" + }, + { + "id": "June", + "translation": "Juni" + }, + { + "id": "July", + "translation": "Juli" + }, + { + "id": "January", + "translation": "Januar" + }, + { + "id": "February", + "translation": "Februar" + }, + { + "id": "December", + "translation": "December" + }, + { + "id": "August", + "translation": "August" + }, + { + "id": "April", + "translation": "April" + }, + { + "id": "api.command_custom_status.name", + "translation": "status" + }, + { + "id": "api.command_custom_status.hint", + "translation": "[:emoji_name:] [status_message] eller slet" + }, + { + "id": "api.command_custom_status.desc", + "translation": "Sæt eller slet din status" + }, + { + "id": "api.command_custom_status.clear.success", + "translation": "Din status blev slettet." + }, + { + "id": "api.command_custom_status.clear.app_error", + "translation": "Fejl ved rømning af status." + }, + { + "id": "api.command_custom_status.app_error", + "translation": "Fejl under oprettelse af status." + }, + { + "id": "api.back_to_app", + "translation": "Tilbage til {{.SiteName}}" + }, + { + "id": "api.admin.add_certificate.parseform.app_error", + "translation": "Fejl ved behandling af forespørgsel på multiformular" + }, + { + "id": "Playbooks", + "translation": "Playbooks" + }, + { + "id": "Channels", + "translation": "Kanaler" + }, + { + "id": "Boards", + "translation": "Boards" + }, + { + "id": "api.admin.saml.invalid_xml_missing_ssoservices.app_error", + "translation": "Manglende identitetsudbyder SSO Services node i XML." + }, + { + "id": "api.admin.saml.invalid_xml_missing_keydescriptor.app_error", + "translation": "Manglende identitetsudbyder Key Descriptors node i XML." + }, + { + "id": "api.admin.saml.invalid_xml_missing_idpssodescriptors.app_error", + "translation": "Manglende identitetsudbyder SSO Descriptors node i XML." + }, + { + "id": "api.admin.saml.failure_reset_authdata_to_email.app_error", + "translation": "Kunne ikke nulstille AuthData felt til e-post." + }, + { + "id": "api.admin.saml.failure_parse_idp_certificate.app_error", + "translation": "Fejlede under fortolkning af metadata information modtaget af identitetsudbyderen af et certifikat." + }, + { + "id": "api.admin.saml.failure_get_metadata_from_idp.app_error", + "translation": "Ikke muligt at finde metadata fra identitetsudbyderens URL." + }, + { + "id": "api.admin.ldap.not_available.app_error", + "translation": "LDAP er ikke tilgængelig." + }, + { + "id": "api.channel.channel_member_counts_by_group.license.error", + "translation": "Deres licens understøtter ikke support grupper" + }, + { + "id": "api.channel.add_members.user_denied", + "translation": "Kanalmedlemskabs nægtet til følgende brugere på grund af gruppebegrænsninger: {{ .UserIDs }}" + }, + { + "id": "api.channel.add_members.error", + "translation": "En fejl opstod ved at tilføje medlemmer til kanalen." + }, + { + "id": "api.bot.teams_channels.add_message_mobile", + "translation": "Tilføj mig til hold og kanaler, som jeg skal interagere med. For at opnå dette, brug browser eller Mattermost skrivebordsapplikation." + }, + { + "id": "api.bot.create_disabled", + "translation": "Oprettelse af bot er blevet deaktiveret." + }, + { + "id": "api.admin.saml.set_certificate_from_metadata.missing_content_type.app_error", + "translation": "Manglende indholdstype." + }, + { + "id": "api.admin.saml.set_certificate_from_metadata.invalid_content_type.app_error", + "translation": "Ugyldig indholdstype." + }, + { + "id": "api.admin.saml.set_certificate_from_metadata.invalid_body.app_error", + "translation": "Ugyldig certifikat tekst." + } +] diff --git a/server/i18n/de.json b/server/i18n/de.json index 73a679d0e1f..ac0988517c1 100644 --- a/server/i18n/de.json +++ b/server/i18n/de.json @@ -1359,10 +1359,6 @@ "other": "{{.Count}} Bilder versendet: {{.Filenames}}" } }, - { - "id": "api.post.link_preview_disabled.app_error", - "translation": "Link-Vorschauen wurden vom Systemadministrator deaktiviert." - }, { "id": "api.post.patch_post.can_not_update_post_in_deleted.error", "translation": "Kann eine Nachricht in einem gelöschten Kanal nicht aktualisieren." @@ -4039,10 +4035,6 @@ "id": "model.emoji.user_id.app_error", "translation": "Ungültige Ersteller-ID." }, - { - "id": "model.file_info.get.gif.app_error", - "translation": "Konnte GIF nicht dekodieren." - }, { "id": "model.file_info.is_valid.create_at.app_error", "translation": "Ungültiger Wert für create_at." @@ -4797,7 +4789,7 @@ }, { "id": "web.error.unsupported_browser.min_browser_version.edge", - "translation": "Version 110+" + "translation": "Version 112+" }, { "id": "web.error.unsupported_browser.min_browser_version.chrome", @@ -5541,7 +5533,7 @@ }, { "id": "api.user.login.invalid_credentials_username", - "translation": "Gültige E-Mail und/oder Passwort eingeben." + "translation": "Gültigen Benutzernamen und/oder Passwort eingeben." }, { "id": "api.user.login.invalid_credentials_sso", @@ -5549,7 +5541,7 @@ }, { "id": "api.user.login.invalid_credentials_email_username", - "translation": "Gültige E-Mail und/oder Passwort eingeben." + "translation": "Gültige E-Mail-Adresse oder Benutzername und/oder Passwort eingeben." }, { "id": "api.user.login.invalid_credentials_email", @@ -5739,10 +5731,6 @@ "id": "api.command_invite.user_not_in_team.app_error", "translation": "@{{.Username}} ist kein Mitglied dieses Teams." }, - { - "id": "api.command_invite.group_constrained_user_denied", - "translation": "Dieser Kanal wird durch Gruppen verwaltet. Dieser Benutzer ist nicht Teil einer Gruppe, die mit diesem Kanal synchronisiert ist." - }, { "id": "api.command.execute_command.format.app_error", "translation": "Dem Kommandowort fehlt der führende Slash" @@ -9106,10 +9094,6 @@ "id": "api.team.invite_members_to_team_and_channels.invalid_body.app_error", "translation": "Ungültiger Request Body." }, - { - "id": "api.insights.feature_disabled", - "translation": "Insights wird durch ein Feature-Flag verborgen, das nicht aktiviert ist." - }, { "id": "api.templates.cloud_upgrade_confirmation.title", "translation": "Du bist jetzt upgegraded worden!" @@ -9126,10 +9110,6 @@ "id": "api.file.cloud_upload.app_error", "translation": "Hochladen über mmctl zu einer Cloud Instanz wird nicht unterstützt. Bitte prüfe die Dokumentation: https://docs.mattermost.com/manage/cloud-data-export.html." }, - { - "id": "app.recent_searches.app_error", - "translation": "Fehler beim Holen der letzten Suchen" - }, { "id": "app.teams.analytics_teams_count.app_error", "translation": "Kann Team-Zähler nicht abfragen" @@ -9154,18 +9134,6 @@ "id": "model.config.is_valid.image_decoder_concurrency.app_error", "translation": "Ungültig Decoder Nebenläufigkeit {{.Value}}. Sollte eine positive Zahl oder -1 sein." }, - { - "id": "app.post.get_top_threads_for_user_since.app_error", - "translation": "Es ist nicht möglich, die Top-Threads für einen Benutzer zu erhalten." - }, - { - "id": "app.post.get_top_threads_for_team_since.app_error", - "translation": "Es ist nicht möglich, Top-Threads für das Team zu erhalten." - }, - { - "id": "app.insights.feature_disabled", - "translation": "Die Insights Funktion ist deaktiviert." - }, { "id": "api.cloud.notify_admin_to_upgrade_error.already_notified", "translation": "Administrator schon benachrichtigt" @@ -9198,10 +9166,6 @@ "id": "app.team.clear_cache.app_error", "translation": "Fehler beim Löschen des Team Mitglieder Cache" }, - { - "id": "app.post.get_top_dms_for_user_since.app_error", - "translation": "Kann die Top DM für den Benutzer nicht holen." - }, { "id": "api.command_marketplace.unsupported.app_error", "translation": "Das Martkplatzkommando wird auf deinem Gerät nicht unterstützt." @@ -9466,14 +9430,6 @@ "id": "api.team.invite_guests_to_channels.disabled.error", "translation": "Gastkonten sind deaktiviert" }, - { - "id": "app.collection.add_topic.exists.app_error", - "translation": "Thementyp existiert schon." - }, - { - "id": "app.collection.add_collection.exists.app_error", - "translation": "Collection Typ existiert schon." - }, { "id": "api.user.add_user_to_group_syncables.not_ldap_user.app_error", "translation": "Kein LDAP-Benutzer" @@ -9598,26 +9554,10 @@ "id": "api.acknowledgement.delete.archived_channel.app_error", "translation": "Du kannst eine Bestätigung in einem archivierten Kanal nicht entfernen." }, - { - "id": "worktemplate.category.product_teams", - "translation": "Produkt" - }, { "id": "model.draft.is_valid.priority.app_error", "translation": "Ungültige Priorität" }, - { - "id": "app.worktemplates.get_templates.app_error", - "translation": "Arbeitsvorlagen können nicht abgerufen werden" - }, - { - "id": "app.worktemplates.get_categories.app_error", - "translation": "Arbeitsvorlagenkategorien können nicht abgerufen werden" - }, - { - "id": "worktemplate.product_teams.feature_release.description.integration", - "translation": "Steigere die Produktivität in deinem Kanal durch die Integration deiner am meistern verwendeten Tools, wie GitHub oder Jira. Diese werden für dich heruntergeladen." - }, { "id": "api.templates.cloud_welcome_email.yearly_plan_button", "translation": "Deine Rechnung einsehen" @@ -9694,78 +9634,14 @@ "id": "api.templates.delinquency_45.subtitle2", "translation": "Ein herabgestufter Arbeitsbereich kann sich negativ auf kritische Arbeitsabläufe und andere geschäftskritische Aktivitäten auswirken, die in deinem Arbeitsbereich durchgeführt werden." }, - { - "id": "app.worktemplate.execution_request.cannot_create_public_playbook", - "translation": "Du hast nicht die Berechtigung, ein öffentliches Playbook zu erstellen." - }, - { - "id": "app.worktemplate.execution_request.cannot_create_public_channel", - "translation": "Du hast keine Berechtigung, einen öffentlichen Kanal zu erstellen." - }, - { - "id": "app.worktemplate.execution_request.cannot_create_public_board", - "translation": "Du hast keine Berechtigung, ein öffentliches Board zu erstellen." - }, - { - "id": "app.worktemplate.execution_request.cannot_create_private_playbook", - "translation": "Du hast keine Berechtigung, ein privates Playbook zu erstellen." - }, - { - "id": "app.worktemplate.execution_request.cannot_create_private_channel", - "translation": "Du hast keine Berechtigung, einen privaten Kanal zu erstellen." - }, - { - "id": "app.worktemplate.execution_request.cannot_create_private_board", - "translation": "Du hast keine Berechtigung, ein privates Board zu erstellen." - }, { "id": "app.group.username_conflict", "translation": "Benutzer mit dem Benutzernamen \"{{.Benutzername}}\" existiert bereits." }, - { - "id": "api.work_templates.disabled", - "translation": "Die Arbeitsvorlagen sind deaktiviert." - }, { "id": "api.config.update_config.translations.app_error", "translation": "Die Serverübersetzungen konnten nicht aktualisiert werden." }, - { - "id": "app.worktemplates.execute_work_template.playbooks.find_channel_error", - "translation": "Kanal, der mit einem Playbook verbunden ist, kann nicht gefunden werden." - }, - { - "id": "worktemplate.category.leadership", - "translation": "Führung" - }, - { - "id": "worktemplate.category.devops", - "translation": "DevOps" - }, - { - "id": "worktemplate.category.companywide", - "translation": "Unternehmensweit" - }, - { - "id": "app.worktemplates.execute_work_template.playbooks.create_error", - "translation": "Fehler beim Erstellen eines Playbooks." - }, - { - "id": "app.worktemplates.execute_work_template.channels.create_error", - "translation": "Fehler beim Erstellen eines Kanals." - }, - { - "id": "app.worktemplates.execute_work_template.boards.create_error", - "translation": "Fehler beim Erstellen eines Boards." - }, - { - "id": "app.worktemplates.execute_work_template.app_error", - "translation": "Fehler beim Ausführen einer Arbeitsvorlage." - }, - { - "id": "app.worktemplate.execution_request.license_cannot_create_private_playbook", - "translation": "Deine Lizenz unterstützt keine privaten Playbooks." - }, { "id": "app.import.validate_user_teams_import_data.invalid_auth_service.error", "translation": "Ungültiger AuthService: {{.AuthService}}" @@ -9774,46 +9650,6 @@ "id": "api.server.cws.needs_enterprise_edition", "translation": "Dienst nur in der Mattermost Enterprise Edition verfügbar" }, - { - "id": "api.command_templates.unsupported.app_error", - "translation": "Der Befehl Vorlagen wird von deinem Gerät nicht unterstützt." - }, - { - "id": "api.command_templates.name", - "translation": "Vorlagen" - }, - { - "id": "api.command_templates.desc", - "translation": "Öffne das Fenster \"Aus Vorlage erstellen\"" - }, - { - "id": "app.team.create_onboarding_linked_board.product_key_not_found", - "translation": "Produktschlüssel nicht gefunden" - }, - { - "id": "app.team.create_onboarding_linked_board.itf_not_of_type", - "translation": "Schnittstelle nicht vom erwarteten Typ" - }, - { - "id": "app.team.create_onboarding_linked_board.error_patching_board", - "translation": "Fehler beim Patchen des Boards" - }, - { - "id": "app.team.create_onboarding_linked_board.error_no_board", - "translation": "Fehler kein Board erstellt" - }, - { - "id": "app.team.create_onboarding_linked_board.error_getting_templates", - "translation": "Fehler beim Abrufen von Board-Templates" - }, - { - "id": "app.team.create_onboarding_linked_board.error_duplicating_board", - "translation": "Fehler beim Duplizieren des Boards" - }, - { - "id": "app.worktemplates.execute_work_template.name_too_long", - "translation": "Das Namensfeld darf nicht mehr als 64 Zeichen enthalten." - }, { "id": "api.system.logs.invalidFilter", "translation": "Ungültiger Protokollfilter" @@ -9830,334 +9666,10 @@ "id": "app.oauth.remove_auth_data_by_client_id.app_error", "translation": "Oauth-Daten können nicht entfernt werden." }, - { - "id": "app.user.run.update_status.title", - "translation": "Aktueller Stand" - }, - { - "id": "app.user.run.update_status.submit_label", - "translation": "Status aktualisieren" - }, - { - "id": "app.user.run.update_status.reminder_for_next_update", - "translation": "Erinnerung an das nächste Update" - }, - { - "id": "app.user.run.update_status.num_channel", - "translation": { - "one": "Bringe die Beteiligten auf den neuesten Stand. Dieser Beitrag wird in einem Kanal veröffentlicht.", - "other": "Bringe die Beteiligten auf den neuesten Stand. Dieser Beitrag wird in {{.Count}} Kanälen veröffentlicht." - } - }, - { - "id": "app.user.run.update_status.finish_run.placeholder", - "translation": "Markiere den Durchlauf auch als beendet" - }, - { - "id": "app.user.run.update_status.finish_run", - "translation": "Durchlauf beenden" - }, - { - "id": "app.user.run.update_status.change_since_last_update", - "translation": "Änderung seit der letzten Aktualisierung" - }, - { - "id": "app.user.run.status_enable", - "translation": "@{{.Username}} hat die Statusaktualisierungen für [{{.RunName}}]({{.RunURL}}) aktiviert" - }, - { - "id": "app.user.run.status_disable", - "translation": "@{{.Username}} hat die Statusaktualisierungen für [{{.RunName}}]({{.RunURL}}) deaktiviert" - }, - { - "id": "app.user.run.request_update", - "translation": "@here — @{{.Name}} hat eine Statusaktualisierung für [{{.RunName}}]({{.RunURL}}) angefordert. \n" - }, - { - "id": "app.user.run.request_join_channel", - "translation": "@{{.Name}} ist ein Teilnehmer und möchte diesem Kanal beitreten. Jedes Mitglied des Kanals kann ihn einladen.\n" - }, - { - "id": "app.user.run.confirm_finish.title", - "translation": "Beenden des Durchlaufs bestätigen" - }, - { - "id": "app.user.run.confirm_finish.submit_label", - "translation": "Durchlauf beenden" - }, - { - "id": "app.user.run.confirm_finish.num_outstanding", - "translation": { - "one": "Es gibt **eine offene Aufgabe**. Bist du sicher, dass du den Durchlauf *{{.RunName}}* für alle Teilnehmer beenden willst?", - "other": "Es gibt **{{.Count}} offene Aufgaben**. Bist du sicher, dass du den Durchlauf *{{.RunName}}* für alle Teilnehmer beenden willst?" - } - }, - { - "id": "app.user.run.add_to_timeline.title", - "translation": "Zur Zeitleiste hinzufügen" - }, - { - "id": "app.user.run.add_to_timeline.summary.placeholder", - "translation": "Kurze Zusammenfassung auf der Zeitachse" - }, - { - "id": "app.user.run.add_to_timeline.summary.help", - "translation": "Max. 64 Zeichen" - }, - { - "id": "app.user.run.add_to_timeline.summary", - "translation": "Zusammenfassung" - }, - { - "id": "app.user.run.add_to_timeline.submit_label", - "translation": "Zur Zeitleiste hinzufügen" - }, - { - "id": "app.user.run.add_to_timeline.playbook_run", - "translation": "Playbook-Durchlauf" - }, - { - "id": "app.user.run.add_checklist_item.title", - "translation": "Neue Aufgabe hinzufügen" - }, - { - "id": "app.user.run.add_checklist_item.submit_label", - "translation": "Aufgabe hinzufügen" - }, - { - "id": "app.user.run.add_checklist_item.name", - "translation": "Name" - }, - { - "id": "app.user.run.add_checklist_item.description", - "translation": "Beschreibung" - }, - { - "id": "app.user.new_run.title", - "translation": "Playbook starten" - }, - { - "id": "app.user.new_run.submit_label", - "translation": "Starte Durchlauf" - }, - { - "id": "app.user.new_run.run_name", - "translation": "Name des Durchlaufs" - }, - { - "id": "app.user.new_run.playbook", - "translation": "Playbook" - }, - { - "id": "app.user.new_run.intro", - "translation": "**Eigentümer** {{.Username}}" - }, - { - "id": "app.user.digest.tasks.zero_assigned", - "translation": "Du hast keine zugewiesene Aufgabe." - }, - { - "id": "app.user.digest.tasks.num_assigned_due_until_today", - "translation": { - "one": "Du hast eine zugewiesene Aufgabe, die jetzt fällig ist:", - "other": "Du hast {{.Count}} zugewiesene Aufgaben, die jetzt fällig sind:" - } - }, - { - "id": "app.user.digest.tasks.num_assigned", - "translation": { - "one": "Du hast eine zugewiesene Aufgabe:", - "other": "Du hast {{.Count}} zugewiesene Aufgaben:" - } - }, - { - "id": "app.user.digest.tasks.heading", - "translation": "Deine zugewiesenen Aufgaben" - }, - { - "id": "app.user.digest.tasks.due_yesterday", - "translation": "Gestern fällig" - }, - { - "id": "app.user.digest.tasks.due_x_days_ago", - "translation": "Fällig vor {{.Count}} Tagen" - }, - { - "id": "app.user.digest.tasks.due_today", - "translation": "Heute fällig" - }, - { - "id": "app.user.digest.tasks.due_in_x_days", - "translation": { - "one": "Fällig in einem Tag", - "other": "Fällig in {{.Count}} Tagen" - } - }, - { - "id": "app.user.digest.tasks.due_after_today", - "translation": { - "one": "Du hast **eine zugewiesene Aufgabe, die nach dem heutige Tag fällig ist**.", - "other": "Du hast **{{.Count}} zugewiesene Aufgaben, die nach dem heutige Tag fällig sind**." - } - }, - { - "id": "app.user.digest.tasks.all_tasks_command", - "translation": "Bitte benutze `/playbook todo` um alle deine Aufgaben anzuzeigen." - }, - { - "id": "app.user.digest.runs_in_progress.zero_in_progress", - "translation": "Du hast keinen aktiven Durchlauf." - }, - { - "id": "app.user.digest.runs_in_progress.num_in_progress", - "translation": { - "one": "Du hast einen aktiven Durchlauf:", - "other": "Du hast {{.Count}} aktive Durchläufe:" - } - }, - { - "id": "app.user.digest.runs_in_progress.heading", - "translation": "Aktive Durchläufe" - }, - { - "id": "app.user.digest.overdue_status_updates.zero_overdue", - "translation": "Du hast keine überfälligen Durchläufe." - }, - { - "id": "app.user.digest.overdue_status_updates.num_overdue", - "translation": { - "one": "Du hast einen überfälligen Durchlauf für ein Statusupdate:", - "other": "Du hast {{.Count}} überfällige Durchläufe für ein Statusupdate:" - } - }, - { - "id": "app.user.digest.overdue_status_updates.heading", - "translation": "Überfällige Statusaktualisierungen" - }, - { - "id": "app.command.execute.error", - "translation": "Kann Befehl nicht ausführen." - }, { "id": "api.license.request-trial.bad-request.business-email", "translation": "Ungültige geschäftliche E-Mail für den Test" }, - { - "id": "worktemplate.product_teams.sprint_planning.integration", - "translation": "Steigere die Produktivität deines Kanals, indem du die am häufigsten verwendeten Tools wie z. B. Zoom integrierst. Diese werden für dich heruntergeladen." - }, - { - "id": "worktemplate.product_teams.sprint_planning.channel", - "translation": "Chatte mit deinem Team in einem Kanal, der sich leicht mit deinen Boards und Integrationen verbinden lässt." - }, - { - "id": "worktemplate.product_teams.sprint_planning.board", - "translation": "Verfolge den Fortschritt deines Teams bei der Erreichung der wöchentlichen Ziele mit Sprintaufteilung, Priorisierung, Zuweisung von Verantwortlichen und Kommentaren." - }, - { - "id": "worktemplate.product_teams.product_roadmap.channel", - "translation": "Chatte mit deinem Team über das Feedback deiner Kunden, setze Prioritäten und stimmt euch gemeinsam über den Fortschritt ab." - }, - { - "id": "worktemplate.product_teams.product_roadmap.board", - "translation": "Verwende das Produkt-Roadmap-Board, um Benutzer-Feedback zu verwalten, Ressourcen zuzuweisen, Ergebnisse in einer Kalenderansicht anzuzeigen und Probleme nach Priorität zu ordnen." - }, - { - "id": "worktemplate.product_teams.goals_and_okrs.integration", - "translation": "Steigere die Produktivität in deinem Kanal, indem du die am häufigsten verwendeten Tools wie Zoom integrierst, um die Zusammenarbeit zu erleichtern. Diese werden für dich heruntergeladen." - }, - { - "id": "worktemplate.product_teams.goals_and_okrs.channel", - "translation": "Chatte mit deinem Team über Ziele und Fortschritte, asynchron oder in Echtzeit, und bleibe über Änderungen in einem einzigen Kanal auf dem Laufenden." - }, - { - "id": "worktemplate.product_teams.goals_and_okrs.board", - "translation": "Verfolge den Fortschritt deines Teams auf dem Weg zu den Unternehmenszielen mit dem Ziele und OKR Board. Halten Besprechungen mit der Besprechungsagenda auf Kurs." - }, - { - "id": "worktemplate.product_teams.feature_release.description.playbook", - "translation": "Fördere die funktionsübergreifende Zusammenarbeit im Team mit Aufgaben-Checklisten und Automatisierungen, die deinen Entwicklungsprozess unterstützen. Führe anschließend eine Retrospektive durch und nimm Verbesserungen für deine nächste Version vor." - }, - { - "id": "worktemplate.product_teams.feature_release.description.channel", - "translation": "Chatte mit deinem Team über alle Release-Blocker und Änderungen in einem Kanal, der sich leicht mit deinen Boards, Playbooks und anderen Integrationen verbinden lässt." - }, - { - "id": "worktemplate.product_teams.feature_release.description.board", - "translation": "Halte Besprechungen mit der Besprechungsagenda auf dem Laufenden. Verwalte dein Arbeitspensum mit dem Projektaufgabenboard." - }, - { - "id": "worktemplate.product_teams.bug_bash.playbook", - "translation": "Verwende Checklisten, um Testbereiche zuzuweisen, und automatisierte Aufgaben, um einen umfassenden Fehlerbehebungsprozess durchzuführen. Nutze eine Retrospektive, um deinen Prozess zu überprüfen und ihn für das nächste Mal zu verbessern." - }, - { - "id": "worktemplate.product_teams.bug_bash.integration", - "translation": "Steigere die Produktivität in deinem Kanal, indem du die am häufigsten verwendeten Tools, wie z. B. Jira, integrierst, um den Fortschritt bei der Fehlerbehebung zu verfolgen. Diese werden für dich heruntergeladen." - }, - { - "id": "worktemplate.product_teams.bug_bash.channel", - "translation": "Plane und verwalte Fehlerberichte und -behebungen in einem einzigen Kanal, der für dein Team und deine Organisation leicht zugänglich ist." - }, - { - "id": "worktemplate.leadership.goals_and_okrs.integration", - "translation": "Steigere die Produktivität in deinem Kanal, indem du die am häufigsten verwendeten Tools wie Zoom integrierst, um die Zusammenarbeit zu erleichtern. Diese werden für dich heruntergeladen." - }, - { - "id": "worktemplate.leadership.goals_and_okrs.channel", - "translation": "Chatte mit deinem Team über Ziele und Fortschritte, asynchron oder in Echtzeit, und bleibe über Änderungen in einem einzigen Kanal auf dem Laufenden." - }, - { - "id": "worktemplate.leadership.goals_and_okrs.board", - "translation": "Verfolge den Fortschritt deines Teams auf dem Weg zu den Unternehmenszielen mit dem Ziele und OKR Board. Halten Besprechungen mit der Besprechungsagenda auf Kurs." - }, - { - "id": "worktemplate.devops.product_release.playbook", - "translation": "Erstelle wiederholbare Arbeitsabläufe, die einfach zu befolgen und zu implementieren sind, damit die Produktveröffentlichungen zuverlässig und pünktlich erfolgen." - }, - { - "id": "worktemplate.devops.product_release.channel", - "translation": "Chatte einfach und schnell mit deinem Team über tägliche Meilensteine, eventuelle Hindernisse und Änderungen an den zu erbringenden Leistungen." - }, - { - "id": "worktemplate.devops.product_release.board", - "translation": "Verwende das Product Release Board, um den Zeitrahmen und den Prozess für die Freigabe zu unterstützen und sicherzustellen, dass jeder weiß, welche Aufgaben fällig sind." - }, - { - "id": "worktemplate.devops.incident_resolution.description.playbook", - "translation": "Nutze Checklisten und Automatisierungen, um wichtige Teammitglieder einzubeziehen, und teile mit, wie der Vorfall gelöst wird." - }, - { - "id": "worktemplate.devops.incident_resolution.description.channel", - "translation": "Chatte mit deinem Team über Prioritäten, füge Beteiligte hinzu, liefere Updates und arbeite an einer Lösung in einem einzigen Kanal." - }, - { - "id": "worktemplate.devops.incident_resolution.description.board", - "translation": "Verwende das Incident Resolution Board, um wiederholbare Prozesse zu unterstützen und definierte Aufgaben im Team zuzuweisen." - }, - { - "id": "worktemplate.companywide.goals_and_okrs.integration", - "translation": "Steigere die Produktivität in deinem Kanal, indem du die am häufigsten verwendeten Tools wie Zoom integrierst, um die Zusammenarbeit zu erleichtern. Diese werden für dich heruntergeladen." - }, - { - "id": "worktemplate.companywide.goals_and_okrs.channel", - "translation": "Chatte mit deinem Team über Ziele und Fortschritte, asynchron oder in Echtzeit, und bleibe über Änderungen in einem einzigen Kanal auf dem Laufenden." - }, - { - "id": "worktemplate.companywide.goals_and_okrs.board", - "translation": "Verfolge den Fortschritt deines Teams auf dem Weg zu den Unternehmenszielen mit dem Ziele und OKR Board. Halten Besprechungen mit der Besprechungsagenda auf Kurs." - }, - { - "id": "worktemplate.companywide.create_project.integration", - "translation": "Steigere die Produktivität deines Kanals durch die Integration am deiner am häufigsten verwendeten Tools. Diese werden für dich heruntergeladen." - }, - { - "id": "worktemplate.companywide.create_project.channel", - "translation": "Chatte mit deinem Team über ein neues Projekt und entscheide, wie es strukturiert werden soll, und zwar in einem Kanal zur Zusammenarbeit." - }, - { - "id": "worktemplate.companywide.create_project.board", - "translation": "Verwend eine Kanban-Board, um deine Projektaufgaben und -fortschritte zu definieren und zu verfolgen." - }, { "id": "model.license_record.is_valid.bytes.app_error", "translation": "Ungültiger Wert für Bytes beim Hochladen einer Lizenz." @@ -10170,12 +9682,288 @@ "id": "app.plugin.skip_installation.app_error", "translation": "Überspringe die Installation des Plugins {{.Id}}, da die vorhandene Version gleich oder neuer ist." }, - { - "id": "app.plugin.blocked.app_error", - "translation": "Das Plugin {{.Id}} steht auf der Blockierliste. Einige Plugins sind blockiert, weil sie in diese Version von Mattermost eingebaut sind." - }, { "id": "api.server.cws.subscribe_to_newsletter.app_error", "translation": "CWS Server kann den Newsletter nicht abonnieren." + }, + { + "id": "api.post.post_priority.persistent_notification_validation_error.request_error", + "translation": "Das Prüfen einer dauerhaften Benachrichtigung ist fehlgeschlagen." + }, + { + "id": "api.post.post_priority.priority_post_not_allowed_for_user.request_error", + "translation": "Der Benutzer darf keine Prioritätsnachrichten oder dauerhafte Benachrichtigungen erstellen." + }, + { + "id": "app.post_persistent_notification.delete_by_channel.app_error", + "translation": "Es ist nicht möglich, die dauerhaften Benachrichtigungen nach Kanal zu löschen." + }, + { + "id": "model.config.is_valid.persistent_notifications_count.app_error", + "translation": "Ungültige Gesamtzahl der dauerhaften Benachrichtigungen pro Beitrag. Muss eine positive Zahl sein." + }, + { + "id": "api.post.post_priority.priority_post_only_allowed_for_root_post.request_error", + "translation": "Nur Hauptnachrichten dürfen Priorität haben." + }, + { + "id": "api.post.post_priority.urgent_persistent_notification_post.request_error", + "translation": "Dauerhafte Benachrichtigungen müssen die Priorität \"Dringend\" haben." + }, + { + "id": "model.config.is_valid.persistent_notifications_recipients.app_error", + "translation": "Ungültige maximale Anzahl von Empfängern für dauerhafte Benachrichtigungen. Muss eine positive Zahl sein." + }, + { + "id": "api.post.post_priority.min_recipients_persistent_notification_post.request_error", + "translation": "Eine dauerhafte Benachrichtigung muss mindestens einen Empfänger haben." + }, + { + "id": "app.post_priority.delete_persistent_notification_post.app_error", + "translation": "Nachricht von dauerhafter Benachrichtigung konnte nicht gelöscht werden" + }, + { + "id": "model.channel_member.is_valid.notify_props.app_error", + "translation": "Überschreitung der Benachrichtigungseigenschaftsgröße melden." + }, + { + "id": "api.license.true_up_review.failed_to_submit", + "translation": "Fehler bei der Übermittlung eines True Up Überprüfungsprofils an CWS." + }, + { + "id": "api.command_exportlink.desc", + "translation": "Erzeuge einen Link zum Herunterladen eines Exports." + }, + { + "id": "api.command_exportlink.driver.app_error", + "translation": "Der Dateispeichertreiber unterstützt die Linkerzeugung nicht." + }, + { + "id": "api.command_exportlink.empty.app_error", + "translation": "Es wurde keine Exportdatei gefunden." + }, + { + "id": "api.command_exportlink.hint", + "translation": "[job-id|zip filename|{{.LatestMsg}}]" + }, + { + "id": "api.command_exportlink.invalid.app_error", + "translation": "Die von dir angeforderte Datei konnte nicht gefunden werden." + }, + { + "id": "api.command_exportlink.link.text", + "translation": "Du kannst deine Datei hier herunterladen: {{.Link}}.\nDieser Link wird in {{.Expiration}} ablaufen." + }, + { + "id": "api.command_exportlink.list.app_error", + "translation": "Kann die Exportliste nicht abrufen." + }, + { + "id": "api.command_exportlink.name", + "translation": "Exportlink" + }, + { + "id": "api.command_exportlink.permission.app_error", + "translation": "Du hast nicht genügend Berechtigungen, um diesen Befehl auszuführen." + }, + { + "id": "api.command_exportlink.presign.app_error", + "translation": "Die Presign-URL kann nicht generiert werden." + }, + { + "id": "api.post.post_priority.max_recipients_persistent_notification_post.request_error", + "translation": "Dauerhafte Benachrichtigungen erlauben maximal {{.MaxRecipients}} Empfänger." + }, + { + "id": "app.channel.get_channels_member_count.existing.app_error", + "translation": "Es ist nicht möglich, die Anzahl der Mitglieder für die angegebenen Kanäle zu ermitteln." + }, + { + "id": "app.channel.get_channels_member_count.find.app_error", + "translation": "Die Anzahl der Mitglieder kann nicht ermittelt werden." + }, + { + "id": "app.channel.update_member.app_error", + "translation": "Die Aktualisierung des Kanalmitglieds ist nicht möglich." + }, + { + "id": "app.channel.update_member.notify_props_limit_exceeded.app_error", + "translation": "Das Kanalmitglied kann nicht aktualisiert werden, da das Größenlimit für die Benachrichtigungseigenschaften überschritten wurde." + }, + { + "id": "app.eport.generate_presigned_url.config.app_error", + "translation": "Diese Aktion erfordert die Verwendung eines speziellen Exportspeichers." + }, + { + "id": "app.eport.generate_presigned_url.driver.app_error", + "translation": "Ihr Exportspeicher-Treiber unterstützt die Erstellung von Vorgabe-URLs nicht." + }, + { + "id": "app.eport.generate_presigned_url.featureflag.app_error", + "translation": "Diese Funktion wird durch ein Feature Flag eingeschränkt." + }, + { + "id": "app.eport.generate_presigned_url.fileexist.app_error", + "translation": "Es konnte nicht geprüft werden, ob die Datei existiert." + }, + { + "id": "app.eport.generate_presigned_url.link.app_error", + "translation": "Die vordefinierte URL kann nicht generiert werden." + }, + { + "id": "app.eport.generate_presigned_url.notfound.app_error", + "translation": "Die Exportdatei wurde nicht gefunden." + }, + { + "id": "model.config.is_valid.persistent_notifications_interval.app_error", + "translation": "Ungültige Häufigkeit der dauerhaften Benachrichtigungen. Muss mindestens zwei Minuten betragen." + }, + { + "id": "api.user.invalidate_password_recovery_tokens.error", + "translation": "Token nach Typ konnten nicht geholt werden, wenn Passwort-Wiederherstellungstoken ungültig gemacht werden" + }, + { + "id": "api.user.invalidate_password_recovery_tokens_delete.error", + "translation": "Token kann nicht entfernt werden, wenn Passwortwiederherstellungstoken ungültig gemacht werden" + }, + { + "id": "api.error_no_organization_name_provided_for_self_hosted_onboarding", + "translation": "Fehler: Kein Organisationsname für selbst gehostetes Onboarding angegeben." + }, + { + "id": "api.user.invalidate_password_recovery_tokens_parse.error", + "translation": "Token kann beim Ungültigmachen von Kennwortwiederherstellungstoken nicht analysiert werden" + }, + { + "id": "app.file_info.get.gif.app_error", + "translation": "Konnte GIF nicht dekodieren." + }, + { + "id": "app.post.delete_post.get_team.app_error", + "translation": "Ein Fehler ist beim Aufrufen des Teams aufgetreten" + }, + { + "id": "app.post_persistent_notification.delete_by_team.app_error", + "translation": "Es ist nicht möglich, die dauerhaften Benachrichtigungen nach Team zu löschen." + }, + { + "id": "app.save_config.plugin_hook_error", + "translation": "Bei der Ausführung des Plugin-Hooks beim Speichern der Konfiguration ist ein Fehler aufgetreten." + }, + { + "id": "model.channel_member.is_valid.channel_auto_follow_threads_value.app_error", + "translation": "Ungültiger channel-auto-follow-threads Wert." + }, + { + "id": "model.config.is_valid.elastic_search.ignored_indexes_dash_prefix.app_error", + "translation": "Ignorierte Indizes für Bereinigung dürfen nicht mit einem Bindestrich beginnen." + }, + { + "id": "model.config.is_valid.local_mode_socket.app_error", + "translation": "Das Verzeichnis der lokalen Socket-Dateien kann nicht gefunden werden." + }, + { + "id": "app.channel.group_message_conversion.channel_member_missing", + "translation": "Kanal-Mitgliedschaft des Benutzers kann nicht gefunden werden" + }, + { + "id": "api.channel.gm_to_channel_conversion.not_allowed_for_user.request_error", + "translation": "Benutzer darf Gruppennachricht nicht in privaten Kanal umwandeln" + }, + { + "id": "app.desktop_token.validate.no_user", + "translation": "Es kann kein Benutzer für dieses Token gefunden werden" + }, + { + "id": "app.channel.get_channels_with_unreads_and_with_mentions.app_error", + "translation": "Ungelesene Nachrichten und Erwähnungen können nicht überprüft werden" + }, + { + "id": "app.reaction.permanent_delete_by_user.app_error", + "translation": "Reaktionen für Benutzer können nicht gelöscht werden." + }, + { + "id": "api.user.login.remote_users.login.error", + "translation": "Die Anmeldung ist fehlgeschlagen, weil Remote-Benutzer sich nicht angemeldet dürfen." + }, + { + "id": "api.command_invite.channel_constrained_user_denied", + "translation": "Dieser Kanal wird durch Gruppen verwaltet. Dieser Benutzer ist nicht Teil einer Gruppe, die mit diesem Kanal synchronisiert wird." + }, + { + "id": "api.channel.update_channel.update_direct_or_group_messages_not_allowed.app_error", + "translation": "Du darfst den Namen, den Anzeigenamen und den Zweck von Direkt- oder Gruppennachrichten nicht ändern." + }, + { + "id": "app.channel.group_message_conversion.original_channel_not_gm", + "translation": "Der zu konvertierende Kanal ist keine Gruppennachricht. Du kannst nur Gruppennachrichten konvertieren" + }, + { + "id": "app.channel.group_message_conversion.post_message.error", + "translation": "Gruppennachricht für Kanalumwandlungsbeitrag konnte nicht erstellt werden" + }, + { + "id": "app.desktop_token.validate.invalid", + "translation": "Token ist ungültig oder abgelaufen" + }, + { + "id": "app.channel.gm_conversion_set_categories.delete_all.error", + "translation": "Vorhandene Sidebar-Kategorien für konvertierte Gruppennachrichten konnten nicht gelöscht werden." + }, + { + "id": "api.command_invite.user_not_in_team.messageOverflow", + "translation": "Du kannst {{.FirstUser}} und {{.Others}} andere zu diesem Kanal hinzufügen, sobald sie Mitglieder des **{{.Team}}**-Teams sind." + }, + { + "id": "api.channel.patch_update_channel.update_direct_or_group_messages_not_allowed.app_error", + "translation": "Du darfst den Namen, den Anzeigenamen und den Zweck von Direkt- oder Gruppennachrichten nicht ändern." + }, + { + "id": "humanize.list_join", + "translation": "{{.OtherItems}} und {{.LastItem}}" + }, + { + "id": "api.command_invite.successOverflow", + "translation": "{{.FirstUser}} und {{.Others}} zum Kanal {{.Channel}} hinzugefügt." + }, + { + "id": "model.config.is_valid.link_metadata_timeout.app_error", + "translation": "Ungültiger Wert für Link-Metadaten-Timeout. Muss eine positive Zahl sein." + }, + { + "id": "app.import.import_channel.deleting.app_error", + "translation": "Der importierte Kanal kann nicht archiviert werden." + }, + { + "id": "api.command_invite.user_already_in_channel.overflow", + "translation": "{{.FirstUser}} und {{.Others}} andere sind bereits im Kanal." + }, + { + "id": "api.channel.group_message.converted.to_private_channel", + "translation": "{{.ConvertedByUsername}} hat diesen Kanal aus einer Gruppennachricht mit {{.GMMembers}} erstellt." + }, + { + "id": "app.thread.mark_all_as_read_by_channels.app_error", + "translation": "Alle Unterhaltungen können nicht als vom Kanal gelesen markiert werden" + }, + { + "id": "app.channel.get_common_teams.incorrect_channel_type", + "translation": "Der Kanal ist keine Gruppennachricht." + }, + { + "id": "app.desktop_token.generateServerToken.invalid_or_expired", + "translation": "Token existiert nicht oder ist abgelaufen" + }, + { + "id": "app.channel.get_common_teams.store_get_common_teams_error", + "translation": "Konnte keine gemeinsamen Teams abrufen." + }, + { + "id": "app.channel.group_message_conversion.incorrect_team", + "translation": "Die in der Konvertierungsanforderung angegebene Team-ID enthält nicht alle Mitglieder der Gruppennachricht" + }, + { + "id": "app.file_info.set_searchable_content.app_error", + "translation": "Der durchsuchbare Inhalt der Datei kann nicht festgelegt werden." } ] diff --git a/server/i18n/el.json b/server/i18n/el.json new file mode 100644 index 00000000000..97d9fb35c76 --- /dev/null +++ b/server/i18n/el.json @@ -0,0 +1,3749 @@ +[ + { + "id": "web.incoming_webhook.disabled.app_error", + "translation": "Οι Εντολές έχουν απενεργοποιηθεί από τον διαχειριστή του συστήματος." + }, + { + "id": "app.user_terms_of_service.delete.app_error", + "translation": "Δεν είναι δυνατή η αποθήκευση των όρων χρήσης της υπηρεσίας." + }, + { + "id": "app.terms_of_service.create.existing.app_error", + "translation": "Δεν πρέπει να κληθεί εκτός των υφιστάμενων όρων χρήσης." + }, + { + "id": "app.scheme.get.app_error", + "translation": "Δεν είναι δυνατή η λήψη του σχήματος." + }, + { + "id": "app.reaction.save.save.app_error", + "translation": "Δεν είναι δυνατή η αποθήκευση της αντίδρασης." + }, + { + "id": "app.emoji.delete.app_error", + "translation": "Δεν είναι δυνατή η διαγραφή του emoji." + }, + { + "id": "store.sql_command.save.get.app_error", + "translation": "Δεν είναι δυνατή η λήψη της εντολής." + }, + { + "id": "app.channel.update.bad_id", + "translation": "Αδυναμία ενημέρωσης του καναλιού." + }, + { + "id": "app.channel.get_deleted.existing.app_error", + "translation": "Δεν είναι δυνατή η εύρεση του υπάρχοντος διαγραμμένου καναλιού." + }, + { + "id": "store.sql_channel.get.existing.app_error", + "translation": "Δεν είναι δυνατή η εύρεση του υπάρχοντος καναλιού." + }, + { + "id": "app.audit.save.saving.app_error", + "translation": "Παρουσιάστηκε σφάλμα κατά την αποθήκευση του ελέγχου." + }, + { + "id": "app.audit.permanent_delete_by_user.app_error", + "translation": "Παρουσιάστηκε σφάλμα κατά τη διαγραφή των ελέγχων." + }, + { + "id": "app.audit.get.finding.app_error", + "translation": "Παρουσιάστηκε σφάλμα κατά την εύρεση των ελέγχων." + }, + { + "id": "mfa.mfa_disabled.app_error", + "translation": "Έλεγχος ταυτότητας πολλαπλών παραγόντων απαιτείται σε αυτόν το διακομιστή." + }, + { + "id": "manaultesting.test_autolink.unable.app_error", + "translation": "Αδυναμία λήψης καναλιών." + }, + { + "id": "interactive_message.generate_trigger_id.signing_failed", + "translation": "Δεν ήταν δυνατή η υπογραφή του αναγνωριστικού ενεργοποίησης που δημιουργήθηκε για το αναδυόμενο παράθυρο." + }, + { + "id": "interactive_message.decode_trigger_id.verify_signature_failed", + "translation": "Απέτυχε η επαλήθευση υπογραφής του αναγνωριστικό ενεργοποίησης." + }, + { + "id": "interactive_message.decode_trigger_id.signature_decode_failed", + "translation": "Απέτυχε η αποκωδικοποίηση base64 του αναγνωριστικού ενεργοποίησης." + }, + { + "id": "interactive_message.decode_trigger_id.missing_data", + "translation": "Το αναγνωριστικό ενεργοποίησης δεν διαθέτει τα απαιτούμενα δεδομένα για το αναδυόμενο παράθυρο." + }, + { + "id": "interactive_message.decode_trigger_id.expired", + "translation": "Το αναγνωριστικό ενεργοποίησης έχει λήξει. Τα αναγνωριστικά ενεργοποίησης διαρκούν το μέγιστο για {{.Seconds}} δευτερόλεπτα." + }, + { + "id": "interactive_message.decode_trigger_id.base64_decode_failed_signature", + "translation": "Απέτυχε η αποκωδικοποίηση base64 του αναγνωριστικού ενεργοποίησης." + }, + { + "id": "interactive_message.decode_trigger_id.base64_decode_failed", + "translation": "Απέτυχε η αποκωδικοποίηση base64 για το αναγνωριστικό του αναδυόμενου παράθυρου." + }, + { + "id": "ent.saml.service_disable.app_error", + "translation": "Το SAML 2.0 δεν έχει ρυθμιστεί ή υποστηρίζεται σε αυτόν τον διακομιστή." + }, + { + "id": "ent.saml.metadata.app_error", + "translation": "Παρουσιάστηκε σφάλμα κατά την κατασκευή παροχέα υπηρεσιών μεταδεδομένων." + }, + { + "id": "ent.message_export.global_relay.create_file_in_zip.app_error", + "translation": "Δεν είναι δυνατή η δημιουργία του αρχείου eml." + }, + { + "id": "ent.message_export.global_relay.close_zip_file.app_error", + "translation": "Δεν είναι δυνατό το κλείσιμο του αρχείου zip." + }, + { + "id": "ent.elasticsearch.index_user.error", + "translation": "Αποτυχία ευρετηρίασης του χρήστη" + }, + { + "id": "ent.elasticsearch.index_post.error", + "translation": "Αποτυχία ευρετηρίασης της ανάρτησης" + }, + { + "id": "ent.elasticsearch.index_channel.error", + "translation": "Αποτυχία ευρετηρίασης του καναλιού" + }, + { + "id": "ent.elasticsearch.delete_user.error", + "translation": "Αποτυχία διαγραφής του χρήστη" + }, + { + "id": "ent.elasticsearch.delete_post.error", + "translation": "Αποτυχία διαγραφής της δημοσίευσης" + }, + { + "id": "ent.elasticsearch.delete_channel.error", + "translation": "Απέτυχε η διαγραφή του καναλιού" + }, + { + "id": "ent.compliance.csv.zip.creation.appError", + "translation": "Δεν είναι δυνατή η δημιουργία του αρχείου εξαγωγής zip." + }, + { + "id": "ent.compliance.csv.warning.appError", + "translation": "Δεν είναι δυνατή η δημιουργία του αρχείου προειδοποίησης." + }, + { + "id": "ent.compliance.csv.metadata.json.zipfile.appError", + "translation": "Δεν είναι δυνατή η δημιουργία του αρχείου zip" + }, + { + "id": "ent.account_migration.get_saml_users_failed", + "translation": "Αδυναμία λήψης χρηστών SAML." + }, + { + "id": "ent.account_migration.get_all_failed", + "translation": "Αδυναμία λήψης χρηστών." + }, + { + "id": "bleveengine.index_user.error", + "translation": "Απέτυχε η ευρετηρίαση του χρήστη." + }, + { + "id": "bleveengine.index_post.error", + "translation": "Απέτυχε η ευρετηρίαση της δημοσίευσης." + }, + { + "id": "bleveengine.index_channel.error", + "translation": "Απέτυχε η ευρετηρίαση του καναλιού." + }, + { + "id": "bleveengine.delete_user.error", + "translation": "Αποτυχία διαγραφής του χρήστη." + }, + { + "id": "bleveengine.delete_post.error", + "translation": "Αποτυχία διαγραφής της δημοσίευσης." + }, + { + "id": "bleveengine.delete_channel.error", + "translation": "Απέτυχε η διαγραφή του καναλιού." + }, + { + "id": "app.submit_interactive_dialog.json_error", + "translation": "Παρουσιάστηκε σφάλμα στη κωδικοποίηση του JSON του αναδυόμενου παράθυρου." + }, + { + "id": "app.plugin.write_file.saving.app_error", + "translation": "Παρουσιάστηκε σφάλμα κατά την αποχώρηση από το κανάλι." + }, + { + "id": "app.plugin.write_file.read.app_error", + "translation": "Παρουσιάστηκε σφάλμα κατά την αποχώρηση από το κανάλι." + }, + { + "id": "app.plugin.remove.app_error", + "translation": "Δεν είναι δυνατή η διαγραφή του plugin." + }, + { + "id": "app.plugin.get_plugins.app_error", + "translation": "Αδυναμία λήψης ενεργών plugins." + }, + { + "id": "app.plugin.delete_public_key.delete.app_error", + "translation": "Προέκυψε σφάλμα κατά τη διαγραφή του δημόσιου κλειδιού." + }, + { + "id": "app.plugin.deactivate.app_error", + "translation": "Δεν είναι δυνατή η απενεργοποίηση του plugin." + }, + { + "id": "app.import.import_direct_channel.update_header_failed.error", + "translation": "Απέτυχε να ενημερωθεί η επικεφαλίδα άμεσου καναλιού" + }, + { + "id": "app.channel.post_update_channel_purpose_message.updated_to", + "translation": "%s ενημέρωσε την κεφαλίδα του καναλιού σε: %s" + }, + { + "id": "app.channel.post_update_channel_purpose_message.updated_from", + "translation": "%s ενημέρωσε την κεφαλίδα του καναλιού από: %s σε: %s" + }, + { + "id": "app.channel.post_update_channel_purpose_message.retrieve_user.error", + "translation": "Αποτυχία ανάκτησης του χρήστη κατά την ενημέρωση της κεφαλίδας του καναλιού %v" + }, + { + "id": "app.channel.post_update_channel_purpose_message.removed", + "translation": "%s αφαίρεσε τη κεφαλίδα του καναλιού (ήταν: %s)" + }, + { + "id": "app.channel.post_update_channel_purpose_message.post.error", + "translation": "Αποτυχία δημοσίευσης μηνύματος συμμετοχής/αποχώρησης" + }, + { + "id": "app.channel.create_direct_channel.internal_error", + "translation": "Αδυναμία αποθήκευσης άμεσου καναλιού." + }, + { + "id": "app.channel.create_channel.internal_error", + "translation": "Δεν είναι δυνατή η αποθήκευση του καναλιού." + }, + { + "id": "app.bot.permenent_delete.bad_id", + "translation": "Δεν είναι δυνατή η διαγραφή του bot." + }, + { + "id": "app.bot.patchbot.internal_error", + "translation": "Αδυναμία ενημέρωσης του bot." + }, + { + "id": "app.bot.getbots.internal_error", + "translation": "Δεν είναι δυνατή η λήψη των bots." + }, + { + "id": "app.bot.getbot.internal_error", + "translation": "Δεν είναι δυνατή η λήψη του bot." + }, + { + "id": "app.bot.createbot.internal_error", + "translation": "Δεν είναι δυνατή η αποθήκευση του bot." + }, + { + "id": "api.websocket_handler.invalid_param.app_error", + "translation": "Μη έγκυρη παράμετρος {{.Name}}." + }, + { + "id": "api.webhook.team_mismatch.app_error", + "translation": "Δεν είναι δυνατή η ενημέρωση των webhook σε όλες τις ομάδες." + }, + { + "id": "api.user.upload_profile_user.too_large.app_error", + "translation": "Αδυναμία μεταφόρτωσης του αρχείου. Το μέγεθός του είναι πολύ μεγάλο." + }, + { + "id": "api.user.upload_profile_user.storage.app_error", + "translation": "Αδυναμία μεταφόρτωσης της εικόνας. Ο χώρος αποθήκευσης των εικόνων δεν έχει ρυθμιστεί." + }, + { + "id": "api.user.upload_profile_user.parse.app_error", + "translation": "Δεν ήταν δυνατή η ανάλυση της φόρμας πολλαπλών κομματιών." + }, + { + "id": "api.user.upload_profile_user.open.app_error", + "translation": "Δεν ήταν δυνατό το άνοιγμα αρχείου πιστοποιητικού." + }, + { + "id": "api.user.upload_profile_user.no_file.app_error", + "translation": "Δεν υπάρχει αρχείο κάτω από την εικόνα στην αίτηση." + }, + { + "id": "api.user.upload_profile_user.array.app_error", + "translation": "Άδειος πίνακας στην εικόνα την αίτησης." + }, + { + "id": "api.user.update_active.permissions.app_error", + "translation": "Δεν έχετε τα κατάλληλα δικαιώματα." + }, + { + "id": "api.user.saml.not_available.app_error", + "translation": "Το SAML 2.0 δεν μπορεί να ρυθμιστεί η δεν υποστηρίζεται από αυτό τον διακομιστή." + }, + { + "id": "api.team.update_team_scheme.scheme_scope.error", + "translation": "Δεν είναι δυνατή η ρύθμιση του σχήματος για το κανάλι, επειδή το παρεχόμενο σχήμα δεν είναι κατάλληλο σχήμα καναλιού." + }, + { + "id": "api.team.update_team_scheme.license.error", + "translation": "Η άδειά σας δεν υποστηρίζει την ενημέρωση ενός σχήματος καναλιού" + }, + { + "id": "api.team.update_member_roles.not_a_member", + "translation": "Ο χρήστης δεν είναι μέλος του συγκεκριμένου καναλιού." + }, + { + "id": "api.team.set_team_icon.too_large.app_error", + "translation": "Αδυναμία μεταφόρτωσης του αρχείου. Το μέγεθός του είναι πολύ μεγάλο." + }, + { + "id": "api.team.set_team_icon.storage.app_error", + "translation": "Αδυναμία μεταφόρτωσης της εικόνας. Ο χώρος αποθήκευσης των εικόνων δεν έχει ρυθμιστεί." + }, + { + "id": "api.team.set_team_icon.parse.app_error", + "translation": "Δεν ήταν δυνατή η ανάλυση της φόρμας πολλαπλών κομματιών." + }, + { + "id": "api.team.set_team_icon.open.app_error", + "translation": "Δεν ήταν δυνατό το άνοιγμα αρχείου πιστοποιητικού." + }, + { + "id": "api.team.set_team_icon.no_file.app_error", + "translation": "Δεν υπάρχει αρχείο κάτω από την εικόνα στην αίτηση." + }, + { + "id": "api.team.set_team_icon.array.app_error", + "translation": "Άδειος πίνακας στην εικόνα την αίτησης." + }, + { + "id": "api.team.remove_user_from_team.removed", + "translation": "%v αφαιρέθηκε από το κανάλι." + }, + { + "id": "api.team.move_channel.post.error", + "translation": "Αποτυχία αποστολής μηνύματος μετακίνησης καναλιού." + }, + { + "id": "api.team.import_team.parse.app_error", + "translation": "Δεν ήταν δυνατή η ανάλυση της φόρμας πολλαπλών κομματιών." + }, + { + "id": "api.team.import_team.open.app_error", + "translation": "Δεν ήταν δυνατό το άνοιγμα αρχείου." + }, + { + "id": "api.team.import_team.no_file.app_error", + "translation": "Δεν υπάρχει αρχείο κάτω από την εικόνα στην αίτηση." + }, + { + "id": "api.team.import_team.array.app_error", + "translation": "Άδειος πίνακας στην εικόνα την αίτησης." + }, + { + "id": "api.team.get_all_teams.insufficient_permissions", + "translation": "Δεν έχετε τα κατάλληλα δικαιώματα για να δείτε όλες τις ομάδες" + }, + { + "id": "api.team.add_user_to_team.added", + "translation": "%v προστέθηκε στο κανάλι από τον/την %v." + }, + { + "id": "api.scheme.patch_scheme.license.error", + "translation": "Η άδειά σας δεν υποστηρίζει την ενημέρωση σχημάτων εξουσιοδότησης" + }, + { + "id": "api.scheme.get_teams_for_scheme.scope.error", + "translation": "Δεν είναι δυνατή η ρύθμιση του σχήματος για το κανάλι, επειδή το παρεχόμενο σχήμα δεν είναι κατάλληλο σχήμα καναλιού." + }, + { + "id": "api.scheme.get_channels_for_scheme.scope.error", + "translation": "Δεν είναι δυνατή η ρύθμιση του σχήματος για το κανάλι, επειδή το παρεχόμενο σχήμα δεν είναι κατάλληλο σχήμα καναλιού." + }, + { + "id": "api.scheme.delete_scheme.license.error", + "translation": "Η άδειά σας δεν υποστηρίζει τη διαγραφή σχημάτων εξουσιοδότησης" + }, + { + "id": "api.scheme.create_scheme.license.error", + "translation": "Η άδειά σας δεν υποστηρίζει τη δημιουργία σχημάτων εξουσιοδότησης." + }, + { + "id": "api.roles.patch_roles.license.error", + "translation": "Η άδειά σας δεν υποστηρίζει περισσότερες εξουσιοδοτήσεις." + }, + { + "id": "api.restricted_system_admin", + "translation": "Αυτή η ενέργεια επιτρέπεται μόνο από τον διαχειριστή του συστήματος." + }, + { + "id": "api.preference.update_preferences.set.app_error", + "translation": "Αδυναμία ορισμού προτιμήσεων χρήστη." + }, + { + "id": "api.preference.preferences_category.get.app_error", + "translation": "Αδυναμία ανάκτησης προτιμήσεων χρήστη." + }, + { + "id": "api.preference.delete_preferences.delete.app_error", + "translation": "Αδυναμία διαγραφής προτιμήσεων χρήστη." + }, + { + "id": "api.post_get_post_by_id.get.app_error", + "translation": "Αδυναμία λήψης ανάρτησης." + }, + { + "id": "api.post.update_post.find.app_error", + "translation": "Αδυναμία εύρεσης της υπάρχουσας ανάρτησης ή σχολίου για ενημέρωση." + }, + { + "id": "api.outgoing_webhook.disabled.app_error", + "translation": "Οι Εντολές έχουν απενεργοποιηθεί από τον διαχειριστή του συστήματος." + }, + { + "id": "api.license.add_license.open.app_error", + "translation": "Δεν ήταν δυνατό το άνοιγμα αρχείου πιστοποιητικού." + }, + { + "id": "api.license.add_license.no_file.app_error", + "translation": "Δεν υπάρχει αρχείο κάτω από την εικόνα στην αίτηση." + }, + { + "id": "api.license.add_license.array.app_error", + "translation": "Άδειος πίνακας στην εικόνα την αίτησης." + }, + { + "id": "api.incoming_webhook.disabled.app_error", + "translation": "Οι Εντολές έχουν απενεργοποιηθεί από τον διαχειριστή του συστήματος." + }, + { + "id": "api.file.upload_file.storage.app_error", + "translation": "Αδυναμία μεταφόρτωσης της εικόνας. Ο χώρος αποθήκευσης των εικόνων δεν έχει ρυθμιστεί." + }, + { + "id": "api.emoji.disabled.app_error", + "translation": "Τα προσαρμοσμένα emoji έχουν απενεργοποιηθεί από τον διαχειριστή του συστήματος." + }, + { + "id": "api.context.session_expired.app_error", + "translation": "Μη έγκυρη ή έχει λήξει η συνεδρία, παρακαλώ συνδεθείτε ξανά." + }, + { + "id": "api.context.permissions.app_error", + "translation": "Δεν έχετε τα κατάλληλα δικαιώματα." + }, + { + "id": "api.context.mfa_required.app_error", + "translation": "Έλεγχος ταυτότητας πολλαπλών παραγόντων απαιτείται σε αυτόν το διακομιστή." + }, + { + "id": "api.context.invalid_param.app_error", + "translation": "Μη έγκυρη παράμετρος {{.Name}}." + }, + { + "id": "api.context.invalid_body_param.app_error", + "translation": "Μη έγκυρο ή ελλιπές {{.Name}} στο αίτημά σας." + }, + { + "id": "api.context.404.app_error", + "translation": "Συγνώμη, δεν μπορέσαμε να βρούμε τη σελίδα." + }, + { + "id": "api.config.update_config.restricted_merge.app_error", + "translation": "Απέτυχε η συγχώνευση της ρύθμισης που έχει δοθεί." + }, + { + "id": "api.command_shrug.name", + "translation": "σήκωμα των ώμων" + }, + { + "id": "api.command_shrug.hint", + "translation": "[μήνυμα]" + }, + { + "id": "api.command_shrug.desc", + "translation": "Προσθέτει ¯\\_(ツ)_/¯ στο μήνυμά σας" + }, + { + "id": "api.command_shortcuts.unsupported.app_error", + "translation": "Η εντολή συντομεύσεων δεν υποστηρίζεται στη συσκευή σας." + }, + { + "id": "api.command_shortcuts.name", + "translation": "συντομεύσεις" + }, + { + "id": "api.command_shortcuts.desc", + "translation": "Εμφανίζει μια λίστα με συντομεύσεις πληκτρολογίου" + }, + { + "id": "api.command_settings.unsupported.app_error", + "translation": "Οι εντολή ρυθμίσεων δεν υποστηρίζεται στη συσκευή σας." + }, + { + "id": "api.command_settings.name", + "translation": "Ρυθμίσεις" + }, + { + "id": "api.command_settings.desc", + "translation": "Ανοίξτε το πλαίσιο διαλόγου Ρυθμίσεις Λογαριασμού" + }, + { + "id": "api.command_search.unsupported.app_error", + "translation": "Η εντολή αναζήτησης δεν υποστηρίζεται στη συσκευή σας." + }, + { + "id": "api.command_search.name", + "translation": "αναζήτηση" + }, + { + "id": "api.command_search.hint", + "translation": "[κείμενο]" + }, + { + "id": "api.command_search.desc", + "translation": "Αναζήτηση κειμένου σε μηνύματα" + }, + { + "id": "api.command_remove.user_not_in_channel", + "translation": "{{.Username}} δεν είναι μέλος αυτού του καναλιού." + }, + { + "id": "api.command_remove.permission.app_error", + "translation": "Δεν έχετε δικαιώματα να αφαιρέσετε τον χρήστη." + }, + { + "id": "api.command_remove.name", + "translation": "Αφαίρεση" + }, + { + "id": "api.command_remove.missing.app_error", + "translation": "Δεν μπορούσαμε να βρούμε το χρήστη. Μπορεί να έχουν απενεργοποιηθεί από τον Διαχειριστή του Συστήματος." + }, + { + "id": "api.command_remove.message.app_error", + "translation": "Ένα μήνυμα πρέπει να παρέχεται με τη εντολή /remove ή με τη εντολή /kick ." + }, + { + "id": "api.command_remove.hint", + "translation": "@[όνομα χρήστη]" + }, + { + "id": "api.command_remove.direct_group.app_error", + "translation": "Δεν μπορείτε να αφαιρέσετε κάποιον από ένα κανάλι άμεσων μηνυμάτων." + }, + { + "id": "api.command_remove.desc", + "translation": "Αφαιρέσετε ένα μέλος από το κανάλι" + }, + { + "id": "api.command_open.name", + "translation": "Ανοιχτό" + }, + { + "id": "api.command_online.success", + "translation": "Τώρα είστε σε κατάσταση Διαθέσιμος" + }, + { + "id": "api.command_online.name", + "translation": "Διαθέσιμος" + }, + { + "id": "api.command_online.desc", + "translation": "Ορίστε τη κατάσταση σας σε Διαθέσιμος" + }, + { + "id": "api.command_offline.success", + "translation": "Τώρα είστε σε κατάσταση εκτός σύνδεσης" + }, + { + "id": "api.command_offline.name", + "translation": "Εκτός σύνδεσης" + }, + { + "id": "api.command_offline.desc", + "translation": "Ορίστε την κατάσταση σας ως εκτός σύνδεσης" + }, + { + "id": "api.command_mute.success_unmute_direct_msg", + "translation": "Αυτό το κανάλι δεν είναι πλέον σε λειτουργία σίγασης." + }, + { + "id": "api.command_mute.success_unmute", + "translation": "Το κανάλι {{.Channel}} δεν είναι πλέον σε λειτουργία σίγασης." + }, + { + "id": "api.command_mute.success_mute_direct_msg", + "translation": "Δεν θα λάβετε ειδοποιήσεις για αυτό το κανάλι μέχρι να απενεργοποιηθεί η σίγαση του καναλιού." + }, + { + "id": "api.command_mute.success_mute", + "translation": "Δε θα λάβετε ειδοποιήσεις για το {{.Channel}} μέχρι να απενεργοποιηθεί η σίγαση του καναλιού." + }, + { + "id": "api.command_mute.not_member.error", + "translation": "Δεν ήταν δυνατή η σίγαση του καναλιού {{.Channel}} καθώς δεν είστε μέλος." + }, + { + "id": "api.command_mute.no_channel.error", + "translation": "Δεν ήταν δυνατή η εύρεση του συγκεκριμένου καναλιού. Χρησιμοποιήστε τη διαχείριση καναλιού για τον εντοπισμό καναλιών." + }, + { + "id": "api.command_mute.name", + "translation": "Σίγαση" + }, + { + "id": "api.command_mute.hint", + "translation": "~[κανάλι]" + }, + { + "id": "api.command_mute.error", + "translation": "Δεν ήταν δυνατή η εύρεση του καναλιού {{.Channel}}. Χρησιμοποιήστε τη [διαχείριση καναλιού] για τον εντοπισμό καναλιών." + }, + { + "id": "api.command_mute.desc", + "translation": "Απενεργοποιεί τις ειδοποιήσεις επιφάνειας εργασίας, ηλεκτρονικού ταχυδρομείου και ώθησης για το τρέχον κανάλι ή το καθορισμένο κανάλι." + }, + { + "id": "api.command_msg.permission.app_error", + "translation": "Δεν έχετε δικαιώματα για άμεσο μήνυμα σε αυτό τον χρήστη." + }, + { + "id": "api.command_msg.name", + "translation": "μήνυμα" + }, + { + "id": "api.command_msg.missing.app_error", + "translation": "Δεν είναι δυνατή η εύρεση του χρήστη." + }, + { + "id": "api.command_msg.hint", + "translation": "@[username] \"μήνυμα\"" + }, + { + "id": "api.command_msg.fail.app_error", + "translation": "Παρουσιάστηκε σφάλμα κατά την αποστολή μηνυμάτων στον χρήστη." + }, + { + "id": "api.command_msg.dm_fail.app_error", + "translation": "Παρουσιάστηκε ένα σφάλμα κατά τη δημιουργία του άμεσου μηνύματος." + }, + { + "id": "api.command_msg.desc", + "translation": "Στείλτε απευθείας μήνυμα σε ένα χρήστη" + }, + { + "id": "api.command_me.name", + "translation": "εγώ" + }, + { + "id": "api.command_me.hint", + "translation": "[μήνυμα]" + }, + { + "id": "api.command_me.desc", + "translation": "Κάντε μία ενέργεια" + }, + { + "id": "api.command_logout.name", + "translation": "Αποσύνδεση" + }, + { + "id": "api.command_logout.desc", + "translation": "Αποσύνδεση" + }, + { + "id": "api.command_leave.name", + "translation": "Αποχώρηση" + }, + { + "id": "api.command_leave.fail.app_error", + "translation": "Παρουσιάστηκε σφάλμα κατά την αποχώρηση από το κανάλι." + }, + { + "id": "api.command_leave.desc", + "translation": "Αποχωρήστε από το τρέχον κανάλι" + }, + { + "id": "api.command_kick.name", + "translation": "Αποβολή" + }, + { + "id": "api.command_join.name", + "translation": "Συμμετοχή" + }, + { + "id": "api.command_join.missing.app_error", + "translation": "Δεν είναι δυνατή η εύρεση του καναλιού." + }, + { + "id": "app.scheme.save.app_error", + "translation": "Δεν είναι δυνατή η δημιουργία του σχήματος." + }, + { + "id": "api.command_join.list.app_error", + "translation": "Παρουσιάστηκε σφάλμα κατά την καταχώρηση καναλιών." + }, + { + "id": "api.command_join.hint", + "translation": "~[κανάλι]" + }, + { + "id": "api.command_join.fail.app_error", + "translation": "Παρουσιάστηκε σφάλμα κατά τη σύνδεση στο κανάλι." + }, + { + "id": "api.command_join.desc", + "translation": "Συμμετάσχετε στο ανοικτό κανάλι" + }, + { + "id": "api.command_invite_people.permission.app_error", + "translation": "Δεν έχετε δικαίωμα να προσκαλέσετε νέους χρήστες σε αυτόν τον διακομιστή." + }, + { + "id": "api.command_invite.user_not_in_team.app_error", + "translation": "{{.Username}} δεν είναι μέλος αυτού του καναλιού." + }, + { + "id": "api.command_invite.user_already_in_channel.app_error", + "translation": "Ο χρήστης {{.User}} είναι ήδη στο κανάλι." + }, + { + "id": "api.command_invite.success", + "translation": "Ο χρήστης {{.User}} προστέθηκε στο κανάλι {{.Το Channel}}." + }, + { + "id": "api.command_invite.private_channel.app_error", + "translation": "Δεν ήταν δυνατή η εύρεση του καναλιού {{.Channel}}. Χρησιμοποιήστε τη διαχείριση του καναλιού για να αναγνωρίσετε κανάλια." + }, + { + "id": "api.command_invite.permission.app_error", + "translation": "Δεν έχετε δικαιώματα για να προσθέσετε τον χρήστη {{.User}} στο κανάλι {{.Channel}}." + }, + { + "id": "api.command_invite.name", + "translation": "Πρόσκληση" + }, + { + "id": "api.command_invite.missing_user.app_error", + "translation": "Δεν μπορούσαμε να βρούμε το χρήστη. Μπορεί να έχουν απενεργοποιηθεί από τον Διαχειριστή του Συστήματος." + }, + { + "id": "api.command_invite.missing_message.app_error", + "translation": "Λείπει το όνομα Χρήστη και το Κανάλι." + }, + { + "id": "api.command_invite.hint", + "translation": "@[όνομα χρήστη] ~[κανάλι]" + }, + { + "id": "api.command_invite.fail.app_error", + "translation": "Παρουσιάστηκε ένα σφάλμα κατά την είσοδό στο κανάλι." + }, + { + "id": "api.command_invite.directchannel.app_error", + "translation": "Δεν μπορείτε να προσθέσετε κάποιον σε ένα απευθείας κανάλι μηνυμάτων." + }, + { + "id": "api.command_invite.desc", + "translation": "Προσκαλέστε ένα χρήστη σε ένα κανάλι" + }, + { + "id": "api.command_invite.channel.error", + "translation": "Δεν ήταν δυνατή η εύρεση του καναλιού {{.Channel}}. Χρησιμοποιήστε τη [διαχείριση καναλιού] για τον εντοπισμό καναλιών." + }, + { + "id": "api.command_invite.channel.app_error", + "translation": "Σφάλμα κατά την ανάκτηση του τρέχον καναλιού." + }, + { + "id": "api.command_help.name", + "translation": "Βοήθεια" + }, + { + "id": "api.command_help.desc", + "translation": "Ανοίξτε τη σελίδα βοήθειας" + }, + { + "id": "api.command_groupmsg.permission.app_error", + "translation": "Δεν έχετε δικαιώματα για να δημιουργήσετε μήνυμα ομάδας." + }, + { + "id": "api.command_groupmsg.name", + "translation": "μήνυμα" + }, + { + "id": "api.command_groupmsg.min_users.app_error", + "translation": "Τα μηνύματα ομάδας περιορίζονται το ελάχιστο σε {{.MinUsers} χρήστες." + }, + { + "id": "api.command_groupmsg.max_users.app_error", + "translation": "Τα μηνύματα ομάδας περιορίζονται το μέγιστο σε {{.MaxUsers}} χρήστες." + }, + { + "id": "api.command_groupmsg.invalid_user.app_error", + "translation": { + "one": "Δεν είναι δυνατή η εύρεση του χρήστη: {{.Users}}", + "other": "Δεν είναι δυνατή η εύρεση των χρηστών: {{.Users}}" + } + }, + { + "id": "api.command_groupmsg.hint", + "translation": "@[username1],@[username2] 'μήνυμα'" + }, + { + "id": "api.command_groupmsg.group_fail.app_error", + "translation": "Παρουσιάστηκε σφάλμα κατά τη δημιουργία του μηνύματος ομάδας." + }, + { + "id": "api.command_groupmsg.fail.app_error", + "translation": "Παρουσιάστηκε σφάλμα κατά την αποστολή μηνυμάτων στους χρήστες." + }, + { + "id": "api.command_groupmsg.desc", + "translation": "Στέλνει ένα μήνυμα ομάδας προς τους συγκεκριμένους χρήστες" + }, + { + "id": "api.command_expand_collapse.fail.app_error", + "translation": "Παρουσιάστηκε σφάλμα κατά την αποστολή μηνυμάτων στους χρήστες." + }, + { + "id": "api.command_expand.name", + "translation": "Επέκταση" + }, + { + "id": "api.command_expand.desc", + "translation": "Ενεργοποιήστε την αυτόματη σύμπτυξη των προεπισκοπήσεων εικόνας" + }, + { + "id": "api.command_echo.message.app_error", + "translation": "Ένα μήνυμα πρέπει να παρέχεται με την εντολή /echo." + }, + { + "id": "api.command_echo.delay.app_error", + "translation": "Οι καθυστερήσεις πρέπει να είναι το μέγιστο 10000 δευτερόλεπτα." + }, + { + "id": "api.command_dnd.success", + "translation": "Η λειτουργία Μην ενοχλείτε είναι ενεργοποιημένη. Δεν θα λάβετε ειδοποιήσεις push για κινητά ή υπολογιστές μέχρι να απενεργοποιηθεί η επιλογή Μην ενοχλείτε." + }, + { + "id": "api.command_dnd.name", + "translation": "Μην ενοχλείτε" + }, + { + "id": "api.command_dnd.desc", + "translation": "Η κατάσταση \"Μην ενοχλείτε\" απενεργοποιεί τις ειδοποιήσεις push για υπολογιστές και κινητά." + }, + { + "id": "api.command_collapse.desc", + "translation": "Ενεργοποιήστε την αυτόματη συμπίεση των προεπισκοπήσεων εικόνας" + }, + { + "id": "api.command_code.name", + "translation": "κώδικας" + }, + { + "id": "api.command_code.message.app_error", + "translation": "Ένα μήνυμα πρέπει να παρέχεται με την εντολή /code." + }, + { + "id": "api.command_code.hint", + "translation": "[κείμενο]" + }, + { + "id": "api.command_code.desc", + "translation": "Εμφάνιση κειμένου ως μπλοκ κώδικα" + }, + { + "id": "api.command_channel_rename.update_channel.app_error", + "translation": "Σφάλμα ενημέρωσης του τρέχοντος καναλιού." + }, + { + "id": "api.command_channel_rename.too_short.app_error", + "translation": "Το όνομα του καναλιού πρέπει να είναι τουλάχιστον {{.Length}} χαρακτήρες." + }, + { + "id": "api.command_channel_rename.too_long.app_error", + "translation": "Το όνομα καναλιού πρέπει να είναι το μέγιστο {{.Length}} χαρακτήρες." + }, + { + "id": "api.command_channel_rename.permission.app_error", + "translation": "Δεν έχετε δικαιώματα για να μετονομάσετε το κανάλι." + }, + { + "id": "api.command_channel_rename.name", + "translation": "Μετονομασία" + }, + { + "id": "api.command_channel_rename.message.app_error", + "translation": "Ένα μήνυμα πρέπει να παρέχεται με την εντολή /rename." + }, + { + "id": "api.command_channel_rename.hint", + "translation": "[κείμενο]" + }, + { + "id": "api.command_channel_rename.direct_group.app_error", + "translation": "Αδύνατο να μετονομάσεις ένα άμεσο μήνυμα στα κανάλια." + }, + { + "id": "api.command_channel_rename.desc", + "translation": "Μετονομασία καναλιού" + }, + { + "id": "api.command_channel_rename.channel.app_error", + "translation": "Σφάλμα κατά την ανάκτηση το τρέχον καναλιού." + }, + { + "id": "api.command_channel_remove.channel.app_error", + "translation": "Σφάλμα κατά την ανάκτηση το τρέχον καναλιού." + }, + { + "id": "api.command_channel_purpose.update_channel.app_error", + "translation": "Σφάλμα κατά την ενημέρωση του τρέχον καναλιού." + }, + { + "id": "api.command_channel_purpose.permission.app_error", + "translation": "Δεν έχετε δικαιώματα για να επεξεργαστείτε το κανάλι." + }, + { + "id": "api.command_channel_purpose.name", + "translation": "Πρόταση" + }, + { + "id": "api.command_channel_purpose.message.app_error", + "translation": "Ένα μήνυμα πρέπει να παρέχεται με εντολή /purpose ." + }, + { + "id": "api.command_channel_purpose.hint", + "translation": "[κείμενο]" + }, + { + "id": "api.command_channel_purpose.channel.app_error", + "translation": "Σφάλμα κατά την ανάκτηση του τρέχον καναλιού." + }, + { + "id": "api.command_channel_header.update_channel.app_error", + "translation": "Σφάλμα κατά την ενημέρωση του τρέχον καναλιού." + }, + { + "id": "api.command_channel_header.permission.app_error", + "translation": "Δεν έχετε δικαιώματα για να επεξεργαστείτε τη κεφαλίδα." + }, + { + "id": "api.command_channel_header.name", + "translation": "Κεφαλίδα" + }, + { + "id": "api.command_channel_header.message.app_error", + "translation": "Ένα μήνυμα πρέπει να παρέχεται με την εντολή /header." + }, + { + "id": "api.command_channel_header.hint", + "translation": "[κείμενο]" + }, + { + "id": "api.command_channel_header.desc", + "translation": "Επεξεργαστείτε τη κεφαλίδα του καναλιού" + }, + { + "id": "api.command_channel_header.channel.app_error", + "translation": "Σφάλμα για να ανακτήσετε το τρέχον κανάλι." + }, + { + "id": "api.command_away.success", + "translation": "Τώρα είσαι σε κατάσταση 'Λείπω'" + }, + { + "id": "api.command_away.name", + "translation": "Λείπω" + }, + { + "id": "api.command_away.desc", + "translation": "Ορίσετε την κατάστασή σας ως 'Λείπω'" + }, + { + "id": "api.command.team_mismatch.app_error", + "translation": "Δεν είναι δυνατή η ενημέρωση εντολών σε όλες τις ομάδες." + }, + { + "id": "api.command.invite_people.sent", + "translation": "Οι προσκλήσεις στάλθηκαν με email" + }, + { + "id": "api.command.invite_people.no_email", + "translation": "Προσδιορίστε μία ή περισσότερες έγκυρες διευθύνσεις ηλεκτρονικού ταχυδρομείου" + }, + { + "id": "api.command.invite_people.name", + "translation": "Προσκαλέστε φίλους" + }, + { + "id": "api.command.invite_people.invite_off", + "translation": "Η δημιουργία λογαριασμών έχει απενεργοποιηθεί σε αυτόν τον διακομιστή, δεν έχουν αποσταλεί προσκλήσεις" + }, + { + "id": "api.command.invite_people.hint", + "translation": "[όνομα@domain.com ...]" + }, + { + "id": "api.command.invite_people.fail", + "translation": "Παρουσιάστηκε ένα σφάλμα κατά την αποστολή προσκλήσεων μέσω email" + }, + { + "id": "api.command.invite_people.email_off", + "translation": "Το ηλεκτρονικό ταχυδρομείο δεν έχει ρυθμιστεί, δεν έχουν αποσταλεί προσκλήσεις" + }, + { + "id": "api.command.invite_people.email_invitations_off", + "translation": "Οι προσκλήσεις μέσω ηλεκτρονικού ταχυδρομείου είναι απενεργοποιημένες, δεν έχουν αποσταλεί προσκλήσεις" + }, + { + "id": "api.command.invite_people.desc", + "translation": "Στείλτε μια πρόσκληση μέσω ηλεκτρονικού ταχυδρομείου στην Mattermost ομάδα σας" + }, + { + "id": "api.command.execute_command.failed.app_error", + "translation": "H χρήση εντολής '{{.Trigger}}' απέτυχε." + }, + { + "id": "api.command.execute_command.create_post_failed.app_error", + "translation": "Εντολή '{{.Trigger}}' απέτυχε να δημοσιεύσετε απάντηση. Παρακαλείστε να επικοινωνήσετε με το Διαχειριστή του συστήματός σας." + }, + { + "id": "api.command.duplicate_trigger.app_error", + "translation": "Αυτή η λέξη είναι ήδη σε χρήση. Παρακαλώ επιλέξτε μια άλλη λέξη." + }, + { + "id": "api.command.disabled.app_error", + "translation": "Οι Εντολές έχουν απενεργοποιηθεί από τον διαχειριστή του συστήματος." + }, + { + "id": "api.command.command_post.forbidden.app_error", + "translation": "Ο χρήστης δεν είναι μέλος του συγκεκριμένου καναλιού." + }, + { + "id": "api.command.admin_only.app_error", + "translation": "Oι ενσωματώσεις περιορίζονται μόνο στους διαχειριστές." + }, + { + "id": "api.channel.update_team_member_roles.scheme_role.app_error", + "translation": "O παρεχόμενος ρόλος διαχειρίζεται από ένα Σχήμα και επομένως δεν μπορεί να εφαρμοστεί απευθείας σε Μέλος της ομάδας." + }, + { + "id": "api.channel.update_channel_scheme.scheme_scope.error", + "translation": "Δεν είναι δυνατή η ρύθμιση του σχήματος για το κανάλι, επειδή το παρεχόμενο σχήμα δεν είναι κατάλληλο σχήμα καναλιού." + }, + { + "id": "api.channel.update_channel_scheme.license.error", + "translation": "Η άδειά σας δεν υποστηρίζει την ενημέρωση ενός σχήματος καναλιού" + }, + { + "id": "api.channel.update_channel_member_roles.scheme_role.app_error", + "translation": "Ο παρεχόμενος ρόλος διαχειρίζεται από ένα Σχέδιο και επομένως δεν μπορεί να εφαρμοστεί απευθείας σε Μέλος του Καναλιού." + }, + { + "id": "api.channel.update_channel.tried.app_error", + "translation": "Προσπαθήσατε να εκτελέσετε μία μη έγκυρη ενημέρωση του προεπιλεγμένου καναλιού {{.Channel}}." + }, + { + "id": "api.channel.update_channel.deleted.app_error", + "translation": "Το κανάλι έχει αρχειοθετηθεί ή διαγραφεί." + }, + { + "id": "api.channel.rename_channel.cant_rename_group_messages.app_error", + "translation": "Δεν μπορείτε να μετονομάσετε ένα μήνυμα ομάδας καναλιού." + }, + { + "id": "api.channel.rename_channel.cant_rename_direct_messages.app_error", + "translation": "Δεν μπορείτε να μετονομάσετε ένα άμεσο μήνυμα καναλιού." + }, + { + "id": "api.channel.remove_user_from_channel.app_error", + "translation": "Δεν είναι δυνατό να προστεθεί χρήστης σε αυτόν τον τύπο καναλιού." + }, + { + "id": "api.channel.remove_member.removed", + "translation": "%v αφαιρέθηκε από το κανάλι." + }, + { + "id": "api.channel.remove_channel_member.type.app_error", + "translation": "Δεν είναι δυνατή η αφαίρεση χρήστη από ένα κανάλι." + }, + { + "id": "api.channel.remove.default.app_error", + "translation": "Δεν είναι δυνατή η αφαίρεση του χρήστη από το προεπιλεγμένο κανάλι {{.Channel}}." + }, + { + "id": "api.channel.post_user_add_remove_message_and_forget.error", + "translation": "Αποτυχία δημοσίευσης μηνύματος συμμετοχής/αποχώρησης" + }, + { + "id": "api.channel.post_update_channel_header_message_and_forget.updated_to", + "translation": "%s ενημέρωσε την κεφαλίδα του καναλιού σε: %s" + }, + { + "id": "api.channel.post_update_channel_header_message_and_forget.updated_from", + "translation": "%s ενημέρωσε την κεφαλίδα του καναλιού από: %s σε: %s" + }, + { + "id": "api.channel.post_update_channel_header_message_and_forget.retrieve_user.error", + "translation": "Αποτυχία ανάκτησης του χρήστη κατά την ενημέρωση της κεφαλίδας του καναλιού" + }, + { + "id": "api.channel.post_update_channel_header_message_and_forget.removed", + "translation": "%s αφαίρεσε τη κεφαλίδα του καναλιού (ήταν: %s)" + }, + { + "id": "api.channel.post_update_channel_header_message_and_forget.post.error", + "translation": "Αποτυχία ενημέρωσης μηνύματος επικεφαλίδας καναλιού" + }, + { + "id": "api.channel.post_update_channel_displayname_message_and_forget.updated_from", + "translation": "%s ενημέρωσε το εμφανιζόμενο όνομα καναλιού από: %s σε: %s" + }, + { + "id": "api.channel.post_update_channel_displayname_message_and_forget.retrieve_user.error", + "translation": "Απέτυχε η ανάκτηση χρήστη κατά την ενημέρωση του πεδίου εμφανιζόμενο όνομα στο κανάλι" + }, + { + "id": "api.channel.post_update_channel_displayname_message_and_forget.create_post.error", + "translation": "Αποτυχία δημοσίευσης μηνύματος ενημέρωσης εμφάνισης" + }, + { + "id": "api.channel.post_channel_privacy_message.error", + "translation": "Αποτυχία δημοσίευσης μηνύματος ενημέρωσης απορρήτου καναλιού." + }, + { + "id": "api.channel.patch_update_channel.forbidden.app_error", + "translation": "Απέτυχε η ενημέρωση του καναλιού." + }, + { + "id": "api.channel.leave.left", + "translation": "%v έφυγε από το κανάλι." + }, + { + "id": "api.channel.leave.last_member.app_error", + "translation": "Είσαι το μόνο μέλος που έμεινε στο κανάλι, δοκίμασε να αλλάξεις 'Ιδιωτικό Κανάλι', αντί να το φύγεις." + }, + { + "id": "api.channel.leave.direct.app_error", + "translation": "Αδύνατο να αφήσετε ένα άμεσο μήνυμα στο κανάλι." + }, + { + "id": "api.channel.leave.default.app_error", + "translation": "Δεν είναι δυνατή η έξοδος από το προεπιλεγμένο κανάλι {{.Channel}}." + }, + { + "id": "api.channel.join_channel.post_and_forget", + "translation": "%v έγινε μέλος του καναλιού." + }, + { + "id": "api.channel.join_channel.permissions.app_error", + "translation": "Δεν έχετε τα κατάλληλα δικαιώματα." + }, + { + "id": "api.channel.delete_channel.type.invalid", + "translation": "Δεν είναι δυνατή η διαγραφή άμεσων ή ομαδικών μηνυμάτων σε κανάλια" + }, + { + "id": "api.channel.delete_channel.deleted.app_error", + "translation": "Το κανάλι έχει αρχειοθετηθεί ή διαγραφεί." + }, + { + "id": "api.channel.delete_channel.cannot.app_error", + "translation": "Δεν είναι δυνατή η διαγραφή του προεπιλεγμένου καναλιού {{.Channel}}." + }, + { + "id": "api.channel.delete_channel.archived", + "translation": "%v αρχειοθέτησε το κανάλι." + }, + { + "id": "api.channel.create_group.bad_user.app_error", + "translation": "Ένας από τους χρήστες δεν υπάρχει." + }, + { + "id": "api.channel.create_group.bad_size.app_error", + "translation": "Τα ομαδικά μηνύματα πρέπει να περιέχουν από 3 έως 8 χρήστες." + }, + { + "id": "api.channel.create_direct_channel.invalid_user.app_error", + "translation": "Μη έγκυρο αναγνωριστικό χρήστη για άμεση δημιουργία καναλιού." + }, + { + "id": "api.channel.create_default_channels.town_square", + "translation": "Town Square" + }, + { + "id": "api.channel.create_default_channels.off_topic", + "translation": "Off-Topic" + }, + { + "id": "api.channel.create_channel.max_channel_limit.app_error", + "translation": "Δεν είναι δυνατή η δημιουργία περισσότερων από {{.MaxChannelsPerTeam}} καναλιών στην τρέχουσα ομάδα." + }, + { + "id": "api.channel.create_channel.direct_channel.app_error", + "translation": "Πρέπει να χρησιμοποιήσετε την υπηρεσία API createDirectChannel για την άμεση δημιουργία καναλιών." + }, + { + "id": "api.channel.change_channel_privacy.public_to_private", + "translation": "Αυτό το κανάλι έχει γίνει ιδιωτικό." + }, + { + "id": "api.channel.change_channel_privacy.private_to_public", + "translation": "Αυτό το κανάλι έχει μετατραπεί σε δημόσιο κανάλι και μπορεί να συμμετέχει οποιοδήποτε μέλος της ομάδας." + }, + { + "id": "api.channel.add_user_to_channel.type.app_error", + "translation": "Δεν είναι δυνατό να προστεθεί χρήστης σε αυτόν τον τύπο καναλιού." + }, + { + "id": "api.channel.add_user.to.channel.failed.deleted.app_error", + "translation": "Αποτυχία προσθήκης χρήστών στο κανάλι, επειδή έχουν αφαιρεθεί από την ομάδα." + }, + { + "id": "api.channel.add_user.to.channel.failed.app_error", + "translation": "Αποτυχία προσθήκης χρήστη στο κανάλι." + }, + { + "id": "api.channel.add_member.added", + "translation": "%v προστέθηκε στο κανάλι από τον/τη %v." + }, + { + "id": "api.channel.add_guest.added", + "translation": "%v προστέθηκε στο κανάλι από τον/τη %v." + }, + { + "id": "api.admin.upload_brand_image.too_large.app_error", + "translation": "Αδυναμία μεταφόρτωσης του αρχείου. Το μέγεθός του είναι πολύ μεγάλο." + }, + { + "id": "api.admin.upload_brand_image.storage.app_error", + "translation": "Αδυναμία μεταφόρτωσης της εικόνας. Ο χώρος αποθήκευσης των εικόνων δεν έχει ρυθμιστεί." + }, + { + "id": "api.admin.upload_brand_image.parse.app_error", + "translation": "Δεν ήταν δυνατή η ανάλυση της φόρμας πολλαπλών κομματιών." + }, + { + "id": "api.admin.upload_brand_image.no_file.app_error", + "translation": "Δεν υπάρχει αρχείο κάτω από την εικόνα στην αίτηση." + }, + { + "id": "api.admin.upload_brand_image.array.app_error", + "translation": "Άδειος πίνακας στην εικόνα την αίτησης." + }, + { + "id": "api.admin.test_s3.missing_s3_bucket", + "translation": "Απαιτείται S3 Bucket" + }, + { + "id": "api.admin.test_email.subject", + "translation": "Δοκιμή ρυθμίσεων Email" + }, + { + "id": "api.admin.test_email.reenter_password", + "translation": "Ο διακομιστής SMTP, η θύρα ή το username έχουν αλλάξει. Παρακαλούμε εισάγετε ξανά το SMTP password για δοκιμή της SMTP σύνδεσης." + }, + { + "id": "api.admin.test_email.missing_server", + "translation": "Απαιτείται διακομιστής SMTP" + }, + { + "id": "api.admin.test_email.body", + "translation": "Φαίνεται ότι το email έχει ρυθμιστεί σωστά!" + }, + { + "id": "api.admin.saml.not_available.app_error", + "translation": "Το SAML 2.0 δεν έχει ρυθμιστεί ή δεν υποστηρίζεται από αυτόν τον διακομιστή." + }, + { + "id": "api.admin.saml.metadata.app_error", + "translation": "Παρουσιάστηκε σφάλμα κατά τη δημιουργία Μεταδεδομένων του Παροχέα Υπηρεσιών." + }, + { + "id": "api.admin.saml.failure_save_idp_certificate_file.app_error", + "translation": "Αδυναμία αποθήκευσης αρχείου πιστοποιητικού." + }, + { + "id": "api.admin.remove_certificate.delete.app_error", + "translation": "Παρουσιάστηκε ένα σφάλμα κατά τη διαγραφή του πιστοποιητικού." + }, + { + "id": "api.admin.get_brand_image.storage.app_error", + "translation": "Ο χώρος αποθήκευσης των εικόνων δεν έχει ρυθμιστεί." + }, + { + "id": "api.admin.file_read_error", + "translation": "Σφάλμα ανάγνωσης αρχείου καταγραφής." + }, + { + "id": "api.admin.add_certificate.saving.app_error", + "translation": "Αδυναμία αποθήκευσης αρχείου πιστοποιητικού." + }, + { + "id": "api.admin.add_certificate.open.app_error", + "translation": "Δεν ήταν δυνατό το άνοιγμα αρχείου πιστοποιητικού." + }, + { + "id": "api.admin.add_certificate.no_file.app_error", + "translation": "Δεν υπάρχει αρχείο στο αίτημα για το 'πιστοποιητικό'." + }, + { + "id": "api.admin.add_certificate.array.app_error", + "translation": "Κανένα αρχείο στο πεδίο \"πιστοποιητικό\" του αιτήματος." + }, + { + "id": "September", + "translation": "Σεπτέμβριος" + }, + { + "id": "October", + "translation": "Οκτώβριος" + }, + { + "id": "November", + "translation": "Νοέμβριος" + }, + { + "id": "May", + "translation": "Μάιος" + }, + { + "id": "March", + "translation": "Μάρτιος" + }, + { + "id": "June", + "translation": "Ιούνιος" + }, + { + "id": "July", + "translation": "Ιούλιος" + }, + { + "id": "January", + "translation": "Ιανουάριος" + }, + { + "id": "February", + "translation": "Φεβρουάριος" + }, + { + "id": "December", + "translation": "Δεκέμβριος" + }, + { + "id": "August", + "translation": "Αύγουστος" + }, + { + "id": "April", + "translation": "Απρίλιος" + }, + { + "id": "web.error.unsupported_browser.system_browser_or", + "translation": "ή" + }, + { + "id": "system.message.name", + "translation": "Σύστημα" + }, + { + "id": "app.import.validate_direct_post_import_data.message_length.error", + "translation": "Το μήνυμα είναι πολύ μεγάλο" + }, + { + "id": "api.server.warn_metric.mfa.start_trial_notification_success.message", + "translation": "Η Εταιρική σας δοκιμαστική έκδοση είναι πλέον ενεργή. Μεταβείτε στην ενότητα **Κονσόλα Συστήματος > Έλεγχος Ταυτότητας > MFA** για την επιβολή ελέγχου ταυτότητας πολλών παραγόντων (MFA)." + }, + { + "id": "api.server.warn_metric.mfa.start_trial.notification_body", + "translation": "Το σύστημά σας της Mattermost διαθέτει έλεγχο ταυτότητας πολλών παραγόντων (MFA), δίνοντας στους χρήστες την επιλογή να ασφαλίσουν τους λογαριασμούς τους με πρόσθετα μέσα ελέγχου ταυτότητας πέρα από έναν κωδικό πρόσβασης. Για να βελτιώσετε την ασφάλεια σε ολόκληρο το σύστημα, μπορείτε να θέσετε σε όλους τους λογαριασμούς Mattermost να χρησιμοποιούν έλεγχο ταυτότητας πολλών παραγόντων (MFA).\n\n[Μάθετε περισσότερα σχετικά με την επιβολή ελέγχου ταυτότητας πολλών παραγόντων (MFA)](https://www.mattermost.com/docs-multi-factor-authentication/?utm_medium=product&utm_source=mattermost-advisor-bot&utm_content=multi-factor-authentication).\n\nΕπιλέγοντας την Έναρξη δοκιμαστικής έκδοσης, συμφωνώ με τις [Συμφωνία Αξιολόγησης Λογισμικού Mattermost](https://mattermost.com/software-evaluation-agaration/), [Πολιτική απορρήτου](https://mattermost.com/privacy-policy/) και λήψη ενημερωτικών email." + }, + { + "id": "api.server.warn_metric.mfa.notification_title", + "translation": "Επιβολή ελέγχου ταυτότητας πολλών παραγόντων (MFA)" + }, + { + "id": "api.server.warn_metric.mfa.notification_body", + "translation": "Το σύστημά σας της Mattermost διαθέτει έλεγχο ταυτότητας πολλών παραγόντων (MFA), δίνοντας στους χρήστες την επιλογή να ασφαλίσουν τους λογαριασμούς τους με πρόσθετα μέσα ελέγχου ταυτότητας πέρα από έναν κωδικό πρόσβασης. Για να βελτιώσετε την ασφάλεια σε ολόκληρο το σύστημα, μπορείτε να θέσετε σε όλους τους λογαριασμούς Mattermost να χρησιμοποιούν έλεγχο ταυτότητας πολλών παραγόντων (MFA).\n\n[Μάθετε περισσότερα σχετικά με την επιβολή ελέγχου ταυτότητας πολλών παραγόντων (MFA)](https://www.mattermost.com/docs-multi-factor-authentication/?utm_medium=product&utm_source=mattermost-advisor-bot&utm_content=multi-factor-authentication).\n\nΚάνοντας κλικ στην επιλογή Επικοινωνήστε μαζί μας, θα μοιραστείτε τις πληροφορίες σας με την Mattermost, Inc. [Μάθετε περισσότερα](https://mattermost.com/pl/default-admin-advisory)" + }, + { + "id": "api.server.warn_metric.mfa.contact_us.email_body", + "translation": "Αίτημα επικοινωνίας με τη Mattermost. Ενδιαφέρομαι να μάθω περισσότερα σχετικά με την εφαρμογή ελέγχου ταυτότητας πολλών παραγόντων (MFA).\n" + }, + { + "id": "api.server.warn_metric.email_us", + "translation": "Στείλτε μας email" + }, + { + "id": "api.server.warn_metric.email_domain.start_trial_notification_success.message", + "translation": "Η Εταιρική σας δοκιμαστική έκδοση είναι πλέον ενεργή. Μεταβείτε στην ενότητα **Κονσόλα Συστήματος> Έλεγχος Ταυτότητας > Πρόσβαση Επισκέπτη** για να ενεργοποιήσετε τους Λογαριασμούς Επισκεπτών." + }, + { + "id": "api.server.warn_metric.email_domain.notification_body", + "translation": "Τα πρότζεκτ συχνά περιλαμβάνουν άτομα εντός και εκτός ενός οργανισμού. Με τους Λογαριασμούς Επισκεπτών, μπορείτε να προσθέσετε εξωτερικούς συνεργάτες στο δικό σας οικοσύστημα του Mattermost και να καθορίσετε με ποιον μπορούν να συνεργαστούν και τι μπορούν να δουν.\n\n[Μάθετε περισσότερα σχετικά με την ενεργοποίηση των Λογαριασμών Επισκεπτών](https://www.mattermost.com/docs-guest-accounts/?utm_medium=product&utm_source=mattermost-advisor-bot&utm_content=guest-accounts).\n\nΚάνοντας κλικ στην επιλογή Επικοινωνήστε μαζί μας, θα μοιραστείτε τις πληροφορίες σας με την Mattermost, Inc. [Μάθετε περισσότερα](https://mattermost.com/pl/default-admin-advisory)" + }, + { + "id": "api.server.warn_metric.email_domain.start_trial.notification_body", + "translation": "Τα πρότζεκτ συχνά περιλαμβάνουν άτομα εντός και εκτός ενός οργανισμού. Με τους Λογαριασμούς Επισκεπτών, μπορείτε να προσθέσετε εξωτερικούς συνεργάτες στο δικό σας οικοσύστημα της Mattermost και να καθορίσετε με ποιον μπορούν να συνεργαστούν και τι μπορούν να δουν.\n\n[Μάθετε περισσότερα σχετικά με την ενεργοποίηση των Λογαριασμών Επισκεπτών](https://www.mattermost.com/docs-guest-accounts/?utm_medium=product&utm_source=mattermost-advisor-bot&utm_content=guest-accounts)\n\nΕπιλέγοντας την Έναρξη δοκιμαστικής έκδοσης, συμφωνώ με τις [Συμφωνία Αξιολόγησης Λογισμικού Mattermost](https://mattermost.com/software-evaluation-agaration/), [Πολιτική απορρήτου](https://mattermost.com/privacy-policy/) και λήψη ενημερωτικών email." + }, + { + "id": "api.server.warn_metric.email_domain.notification_title", + "translation": "Δημιουργία Λογαριασμών Επισκεπτών" + }, + { + "id": "api.server.warn_metric.email_domain.contact_us.email_body", + "translation": "Αίτημα επικοινωνίας με τη Mattermost. Ενδιαφέρομαι να μάθω περισσότερα σχετικά με τη χρήση Λογαριασμών Επισκεπτών.\n" + }, + { + "id": "api.server.warn_metric.bot_response.start_trial_failure.message", + "translation": "Δεν ήταν δυνατή η ανάκτηση της δοκιμαστικής άδειας. Επισκεφτείτε τη διεύθυνση https://mattermost.com/trial/ για να αποκτήσετε μία." + }, + { + "id": "api.server.warn_metric.bot_response.notification_success.message", + "translation": "Σας ευχαριστούμε που επικοινωνήσατε με τη Mattermost. Θα επικοινωνήσουμε μαζί σας το συντομότερο δυνατόν." + }, + { + "id": "api.server.warn_metric.bot_response.notification_failure.message", + "translation": "Δεν ήταν δυνατή η αποστολή μηνύματος." + }, + { + "id": "api.server.warn_metric.bot_response.notification_failure.body", + "translation": "Παρακαλώ στείλτε μας email." + }, + { + "id": "api.server.warn_metric.bot_response.mailto_subject", + "translation": "Αίτημα επικοινωνίας με τη Mattermost" + }, + { + "id": "api.server.warn_metric.bot_response.mailto_site_url_header", + "translation": "Διεύθυνση ιστότοπου: {{.SiteUrl}}" + }, + { + "id": "api.server.warn_metric.bot_response.mailto_registered_users_header", + "translation": "Σύνολο ενεργών χρηστών: {{.NoRegisteredUsers}}" + }, + { + "id": "api.server.warn_metric.bot_response.mailto_footer", + "translation": "Εάν έχετε επιπλέον απορίες, παρακαλώ επικοινωνήστε με το support@mattermost.com" + }, + { + "id": "api.server.warn_metric.bot_response.mailto_email_header", + "translation": "Διεύθυνση ηλεκτρονικού ταχυδρομείου: {{.Email}}" + }, + { + "id": "api.server.warn_metric.bot_response.mailto_diagnostic_id_header", + "translation": "Αναγνωριστικό διάγνωσης: {{.DiagnosticId}}" + }, + { + "id": "api.server.warn_metric.bot_response.mailto_contact_header", + "translation": "Επικοινωνία: {{.Contact}}" + }, + { + "id": "api.server.start_server.starting.critical", + "translation": "Σφάλμα έναρξης διακομιστή, err:%v" + }, + { + "id": "api.server.start_server.rate_limiting_rate_limiter", + "translation": "Δεν είναι δυνατή η προετοιμασία του περιορισμού τιμών." + }, + { + "id": "api.server.start_server.rate_limiting_memory_store", + "translation": "Δεν είναι δυνατή η προετοιμασία που περιορίζει τον ρυθμό αποθήκευσης της μνήμης. Ελέγξτε τη ρύθμιση διαμόρφωσης του MemoryStoreSize." + }, + { + "id": "api.server.start_server.forward80to443.enabled_but_listening_on_wrong_port", + "translation": "Δεν είναι δυνατή η προώθηση της θύρας 80 στη θύρα 443 ενώ γίνεται ακρόαση στη θύρα %s: απενεργοποιήστε το Forward80To443 εάν χρησιμοποιείτε διακομιστή διαμεσολάβησης (proxy)" + }, + { + "id": "api.server.start_server.forward80to443.disabled_while_using_lets_encrypt", + "translation": "Πρέπει να ενεργοποιήσετε το Forward80To443 όταν χρησιμοποιείτε το LetsEncrypt" + }, + { + "id": "api.roles.patch_roles.not_allowed_permission.error", + "translation": "Δεν επιτρέπεται ένα ή περισσότερα από τα ακόλουθα δικαιώματα που προσπαθείτε να προσθέσετε ή να καταργήσετε" + }, + { + "id": "api.remote_cluster.update_not_unique.app_error", + "translation": "Υπάρχει ήδη ασφαλής σύνδεση με την ίδια διεύθυνση ιστοσελίδας." + }, + { + "id": "api.remote_cluster.update.app_error", + "translation": "Παρουσιάστηκε σφάλμα κατά την ενημέρωση της ασφαλούς σύνδεσης." + }, + { + "id": "api.remote_cluster.service_not_enabled.app_error", + "translation": "Η υπηρεσία απομακρυσμένου cluster δεν είναι ενεργοποιημένη." + }, + { + "id": "api.remote_cluster.save_not_unique.app_error", + "translation": "Η ασφαλής σύνδεση έχει ήδη προστεθεί." + }, + { + "id": "api.remote_cluster.save.app_error", + "translation": "Παρουσιάστηκε σφάλμα κατά την αποθήκευση της ασφαλούς σύνδεσης." + }, + { + "id": "api.remote_cluster.invalid_topic.app_error", + "translation": "Μη έγκυρο θέμα." + }, + { + "id": "api.remote_cluster.invalid_id.app_error", + "translation": "Μη έγκυρο αναγνωριστικό." + }, + { + "id": "api.remote_cluster.get.app_error", + "translation": "Παρουσιάστηκε σφάλμα κατά την ανάκτηση μιας ασφαλούς σύνδεσης." + }, + { + "id": "api.remote_cluster.delete.app_error", + "translation": "Παρουσιάστηκε σφάλμα κατά τη διαγραφή της ασφαλούς σύνδεσης." + }, + { + "id": "api.reaction.save_reaction.user_id.app_error", + "translation": "Δεν μπορείτε να αποθηκεύσετε μία αντίδραση για άλλον χρήστη." + }, + { + "id": "api.reaction.save_reaction.invalid.app_error", + "translation": "Η αντίδραση δεν είναι έγκυρη." + }, + { + "id": "api.reaction.save.archived_channel.app_error", + "translation": "Δεν μπορείτε να βάλετε αντίδραση σε ένα αρχειοθετημένο κανάλι." + }, + { + "id": "api.reaction.delete.archived_channel.app_error", + "translation": "Δεν μπορείτε να καταργήσετε μια αντίδραση σε ένα αρχειοθετημένο κανάλι." + }, + { + "id": "api.push_notifications_ack.message.parse.app_error", + "translation": "Παρουσιάστηκε σφάλμα κατά τη δημιουργία του μηνύματος ack μέσω push ειδοποίησης." + }, + { + "id": "api.push_notifications_ack.forward.app_error", + "translation": "Παρουσιάστηκε σφάλμα κατά την αποστολή της απόδειξης παράδοσης στην υπηρεσία ειδοποιήσεων push." + }, + { + "id": "api.push_notifications.session.expired", + "translation": "Η περίοδος σύνδεσης έληξε: Συνδεθείτε για να συνεχίσετε να λαμβάνετε ειδοποιήσεις. Οι περίοδοι σύνδεσης για το {{.siteName}} διαμορφώνονται από τον Διαχειριστή συστήματος για να λήγουν κάθε {{.daysCount}} ημέρες." + }, + { + "id": "api.push_notification.disabled.app_error", + "translation": "Οι ειδοποιήσεις push είναι απενεργοποιημένες σε αυτόν τον διακομιστή." + }, + { + "id": "api.push_notifications.message.parse.app_error", + "translation": "Παρουσιάστηκε σφάλμα κατά τη δημιουργία του μηνύματος ειδοποίησης push." + }, + { + "id": "api.push_notification.id_loaded.fetch.app_error", + "translation": "Παρουσιάστηκε σφάλμα κατά την ανάκτηση της ειδοποίησης push που έχει φορτωθεί με αναγνωριστικό." + }, + { + "id": "api.push_notification.id_loaded.default_message", + "translation": "Έχετε λάβει ένα νέο μήνυμα." + }, + { + "id": "api.preference.update_preferences.update_sidebar.app_error", + "translation": "Αδυναμία ενημέρωσης της πλευρικής γραμμής για να ταιριάζει με τις ενημερωμένες προτιμήσεις" + }, + { + "id": "api.preference.delete_preferences.update_sidebar.app_error", + "translation": "Αδυναμία ενημέρωσης της πλευρικής γραμμής για αντιστοίχιση των διαγραμμένων προτιμήσεων" + }, + { + "id": "api.command.execute_command.start.app_error", + "translation": "Δε βρέθηκε η εντολή ενεργοποίησης." + }, + { + "id": "api.command.execute_command.not_found.app_error", + "translation": "Δε βρέθηκε η εντολή \"{{.Trigger}}\". Για να στείλετε ένα μήνυμα που αρχίζει με \"/\", δοκιμάστε να προσθέσετε έναν κενό χαρακτήρα στην αρχή του μηνύματος." + }, + { + "id": "api.command.execute_command.format.app_error", + "translation": "Λείπει η λέξη ενεργοποίησης εντολών που ξεκινάει με κάθετο χαρακτήρα" + }, + { + "id": "api.command.execute_command.failed_resp.app_error", + "translation": "H χρήση εντολής '{{.Trigger}}' επέστρεψε απάντηση {{.Status}}." + }, + { + "id": "api.command.execute_command.failed_empty.app_error", + "translation": "H χρήση εντολής '{{.Trigger}}' επέστρεψε μια κενή απάντηση." + }, + { + "id": "api.server.warn_metric.number_of_active_users_200.notification_body", + "translation": "Το σύστημά σας Mattermost έχει τώρα 200 χρήστες. Όταν συνδέετε το Mattermost με το single sign-on (SSO) του οργανισμού σας, οι χρήστες μπορούν να έχουν πρόσβαση στο Mattermost χωρίς να χρειάζεται να εισαγάγουν ξανά τα διαπιστευτήριά τους. Σας συνιστούμε να ενσωματώσετε τον παροχέα SAML 2.0 στον διακομιστή του Mattermost. [Μάθετε περισσότερα σχετικά με την ενοποίηση με το SAML 2.0](https://www.mattermost.com/docs-saml/?utm_medium=product&utm_source=mattermost-advisor-bot&utm_content=saml).\n\nΚάνοντας κλικ στην επιλογή Επικοινωνήστε μαζί μας, θα μοιραστείτε τις πληροφορίες σας με τη Mattermost, Inc. [Μάθετε περισσότερα](https://mattermost.com/pl/default-admin-advisory)" + }, + { + "id": "api.server.warn_metric.number_of_active_users_200.contact_us.email_body", + "translation": "Αίτημα επικοινωνίας με τη Mattermost. Η ομάδα μου έχει τώρα 200 χρήστες και σκέφτομαι την Εταιρική έκδοση της Mattermost.\n" + }, + { + "id": "api.server.warn_metric.number_of_active_users_100.start_trial.notification_success.message", + "translation": "Η Εταιρική σας δοκιμαστική έκδοση είναι πλέον ενεργή. Μεταβείτε στην ενότητα **Κονσόλα συστήματος > Έλεγχος ταυτότητας > AD/LDAP** για να ενσωματώσετε το δικό σας Active Directory/LDAP." + }, + { + "id": "api.server.warn_metric.number_of_active_users_100.start_trial.notification_body", + "translation": "Το σύστημά σας Mattermost έχει περισσότερους από 100 χρήστες. Καθώς αυξάνεται ο αριθμός χρηστών σας, η παροχή νέων λογαριασμών μπορεί να είναι χρονοβόρα. Σας συνιστούμε να ενσωματώσετε τον Active Directory/LDAP του οργανισμού σας, το οποίο θα επιτρέπει σε οποιονδήποτε με λογαριασμό να έχει πρόσβαση και στο Mattermost.\n\n[Μάθετε περισσότερα σχετικά με την ενσωμάτωση με Active Directory/LDAP](https://www.mattermost.com/docs-adldap/?utm_medium=product&utm_source=mattermost-advisor-bot&utm_content=adldap)\n\nΕπιλέγοντας την Έναρξη δοκιμαστικής έκδοσης, συμφωνώ με τις [Συμφωνία Αξιολόγησης Λογισμικού Mattermost](https://mattermost.com/software-evaluation-agaration/), [Πολιτική απορρήτου](https://mattermost.com/privacy-policy/) και λήψη ενημερωτικών email." + }, + { + "id": "api.server.warn_metric.number_of_active_users_100.notification_title", + "translation": "Αναπτυχτείτε μαζί με το Mattermost" + }, + { + "id": "api.server.warn_metric.number_of_active_users_100.notification_body", + "translation": "Το σύστημά σας Mattermost έχει περισσότερους από 100 χρήστες. Καθώς αυξάνεται ο αριθμός χρηστών σας, η παροχή νέων λογαριασμών μπορεί να είναι χρονοβόρα. Σας συνιστούμε να ενσωματώσετε τον Active Directory/LDAP του οργανισμού σας, το οποίο θα επιτρέπει σε οποιονδήποτε με λογαριασμό να έχει πρόσβαση και στο Mattermost.\n\n[Μάθετε περισσότερα σχετικά με την ενσωμάτωση με Active Directory/LDAP](https://www.mattermost.com/docs-adldap/?utm_medium=product&utm_source=mattermost-advisor-bot&utm_content=adldap)\n\nΚάνοντας κλικ στην επιλογή Επικοινωνήστε μαζί μας, θα μοιραστείτε τις πληροφορίες σας με τη Mattermost, Inc. [Μάθετε περισσότερα](https://mattermost.com/pl/default-admin-advisory)" + }, + { + "id": "api.server.warn_metric.number_of_active_users_100.contact_us.email_body", + "translation": "Αίτημα επικοινωνίας με τη Mattermost. Η ομάδα μου έχει τώρα 100 χρήστες και σκέφτομαι την Εταιρική έκδοση της Mattermost.\n" + }, + { + "id": "api.server.warn_metric.number_of_channels_50.notification_title", + "translation": "Χρήση Προηγμένων δικαιωμάτων" + }, + { + "id": "api.server.warn_metric.number_of_channels_50.notification_body", + "translation": "Τα κανάλια συμβάλλουν στη βελτίωση της επικοινωνίας, αλλά με τους χρήστες στο Mattermost να συμμετέχουν και να δημιουργούν κανάλια, η πρόκληση να διατηρηθεί το σύστημα οργανωμένο αυξάνεται. Τα Προηγμένα δικαιώματα σάς δίνουν τη δυνατότητα να ορίσετε ποιοι χρήστες ή ρόλοι μπορούν να εκτελέσουν συγκεκριμένες ενέργειες, συμπεριλαμβανομένης της διαχείρισης ρυθμίσεων καναλιού και μελών, χρησιμοποιώντας το @channel ή το @here για να επισημάνετε ευρείες ομάδες χρηστών και τη δημιουργία νέων webhooks.\n\n[Μάθετε περισσότερα σχετικά με τη χρήση Προηγμένων δικαιωμάτων](https://www.mattermost.com/docs-advanced-permissions/?utm_medium=product&utm_source=mattermost-advisor-bot&utm_content=advanced-permissions)\n\nΚάνοντας κλικ στην επιλογή Επικοινωνήστε μαζί μας, θα μοιραστείτε τις πληροφορίες σας με τη Mattermost, Inc. [Μάθετε περισσότερα](https://mattermost.com/pl/default-admin-advisory)" + }, + { + "id": "api.server.warn_metric.number_of_channels_50.contact_us.email_body", + "translation": "Αίτημα επικοινωνίας με τη Mattermost. Ενδιαφέρομαι να μάθω περισσότερα σχετικά με τη χρήση των προηγμένων δικαιωμάτων με σχήματα συστήματος.\n" + }, + { + "id": "api.server.warn_metric.number_of_active_users_500.start_trial.notification_success.message", + "translation": "Η Εταιρική σας δοκιμαστική έκδοση είναι πλέον ενεργή. Μεταβείτε στην Κονσόλα συστήματος για να ενεργοποιήσετε τις προηγμένες δυνατότητες." + }, + { + "id": "api.server.warn_metric.number_of_active_users_500.start_trial.notification_body", + "translation": "Η Mattermost συνιστά ανεπιφύλακτα ότι τα συστήματα άνω των 500 χρηστών αξιοποιούν λειτουργίες όπως η διαχείριση χρηστών, η ομαδοποίηση διακομιστών και η παρακολούθηση επιδόσεων. Επικοινωνήστε μαζί μας για να μάθετε περισσότερα και πείτε μας πώς μπορούμε να σας βοηθήσουμε.\n\nΕπιλέγοντας την Έναρξη δοκιμαστικής έκδοσης, συμφωνώ με τις [Συμφωνία Αξιολόγησης Λογισμικού Mattermost](https://mattermost.com/software-evaluation-agaration/), [Πολιτική απορρήτου](https://mattermost.com/privacy-policy/) και λήψη ενημερωτικών email." + }, + { + "id": "api.server.warn_metric.number_of_active_users_500.notification_title", + "translation": "Αναπτυχτείτε μαζί με το Mattermost" + }, + { + "id": "api.server.warn_metric.number_of_active_users_500.notification_body", + "translation": "Η Mattermost συνιστά ανεπιφύλακτα ότι τα συστήματα άνω των 500 χρηστών αξιοποιούν λειτουργίες όπως η διαχείριση χρηστών, η ομαδοποίηση διακομιστών και η παρακολούθηση επιδόσεων. Επικοινωνήστε μαζί μας για να μάθετε περισσότερα και πείτε μας πώς μπορούμε να σας βοηθήσουμε.\n\nΚάνοντας κλικ στην επιλογή Επικοινωνήστε μαζί μας, θα μοιραστείτε τις πληροφορίες σας με τη Mattermost, Inc. [Μάθετε περισσότερα](https://mattermost.com/pl/default-admin-advisory)" + }, + { + "id": "api.server.warn_metric.number_of_active_users_500.contact_us.email_body", + "translation": "Αίτημα επικοινωνίας με τη Mattermost. Η ομάδα μου έχει τώρα 500 χρήστες και σκέφτομαι την Εταιρική έκδοση της Mattermost.\n" + }, + { + "id": "api.server.warn_metric.number_of_active_users_300.start_trial.notification_title", + "translation": "Κανάλια Ανακοίνωσης μόνο για ανάγνωση" + }, + { + "id": "api.server.warn_metric.number_of_active_users_300.start_trial.notification_success.message", + "translation": "Η Εταιρική σας δοκιμαστική έκδοση είναι πλέον ενεργή. Δημιουργήστε ένα κανάλι και μεταβείτε στην ενότητα **Κονσόλα συστήματος > Διαχείριση χρηστών > Κανάλια** για να περιορίσετε την ανάρτηση δημοσιεύσεων μόνο στους διαχειριστές καναλιών." + }, + { + "id": "api.server.warn_metric.number_of_active_users_300.start_trial.notification_body", + "translation": "Με τόσες πολλές συνομιλίες στο Mattermost, μπορεί να είναι δύσκολο να γνωρίζουμε πού να αναζητήσουμε σημαντικές πληροφορίες. Αν θέλετε να μεταδώσετε ένα μήνυμα σε μεγάλο κοινό, μπορείτε να ρυθμίσετε Κανάλια Ανακοίνωσης τα οποία είναι μόνο για ανάγνωση, όπου μπορούν να συμμετέχουν όλοι, αλλά μόνο οι διαχειριστές καναλιών μπορούν να δημοσιεύουν μηνύματα.\n\n[Μάθετε περισσότερα σχετικά με τη δημιουργία Καναλιών Ανακοίνωσης μόνο για ανάγνωση](https://www.mattermost.com/docs-channel-moderation/?utm_medium=product&utm_source=mattermost-advisor-bot&utm_content=channel-moderation)\n\nΕπιλέγοντας την Έναρξη δοκιμαστικής έκδοσης, συμφωνώ με τις [Συμφωνία Αξιολόγησης Λογισμικού Mattermost](https://mattermost.com/software-evaluation-agaration/), [Πολιτική απορρήτου](https://mattermost.com/privacy-policy/) και λήψη ενημερωτικών email." + }, + { + "id": "api.server.warn_metric.number_of_active_users_300.notification_body", + "translation": "Με τόσες πολλές συνομιλίες στο Mattermost, μπορεί να είναι δύσκολο να γνωρίζουμε πού να αναζητήσουμε σημαντικές πληροφορίες. Αν θέλετε να μεταδώσετε ένα μήνυμα σε μεγάλο κοινό, μπορείτε να ρυθμίσετε Κανάλια Ανακοίνωσης τα οποία είναι μόνο για ανάγνωση, όπου μπορούν να συμμετέχουν όλοι, αλλά μόνο οι διαχειριστές καναλιών μπορούν να δημοσιεύουν μηνύματα.\n\n[Μάθετε περισσότερα σχετικά με τη δημιουργία Καναλιών Ανακοίνωσης μόνο για ανάγνωση](https://www.mattermost.com/docs-channel-moderation/?utm_medium=product&utm_source=mattermost-advisor-bot&utm_content=channel-moderation)\n\nΚάνοντας κλικ στην επιλογή Επικοινωνήστε μαζί μας, θα μοιραστείτε τις πληροφορίες σας με τη Mattermost, Inc. [Μάθετε περισσότερα](https://mattermost.com/pl/default-admin-advisory)" + }, + { + "id": "api.server.warn_metric.number_of_active_users_300.contact_us.email_body", + "translation": "Αίτημα επικοινωνίας με τη Mattermost. Ενδιαφέρομαι να μάθω περισσότερα σχετικά με τη δημιουργία Καναλιών Ανακοίνωσης τα οποία είναι μόνο για ανάγνωση.\n" + }, + { + "id": "api.server.warn_metric.number_of_active_users_200.start_trial.notification_success.message", + "translation": "Η Εταιρική σας δοκιμαστική έκδοση είναι πλέον ενεργή. Μεταβείτε στην ενότητα **Κονσόλα συστήματος > Έλεγχος ταυτότητας > SAML 2.0** για να γίνει ενοποίηση με τον παροχέα σας SAML 2.0." + }, + { + "id": "api.server.warn_metric.number_of_active_users_200.start_trial.notification_body", + "translation": "Το σύστημά σας Mattermost έχει τώρα 200 χρήστες. Όταν συνδέετε το Mattermost με το single sign-on (SSO) του οργανισμού σας, οι χρήστες μπορούν να έχουν πρόσβαση στο Mattermost χωρίς να χρειάζεται να εισαγάγουν ξανά τα διαπιστευτήριά τους. Σας συνιστούμε να ενσωματώσετε τον παροχέα SAML 2.0 στον διακομιστή του Mattermost. [Μάθετε περισσότερα σχετικά με την ενοποίηση με το SAML 2.0](https://www.mattermost.com/docs-saml/?utm_medium=product&utm_source=mattermost-advisor-bot&utm_content=saml).\n\nΕπιλέγοντας την Έναρξη δοκιμαστικής έκδοσης, συμφωνώ με τις [Συμφωνία Αξιολόγησης Λογισμικού Mattermost](https://mattermost.com/software-evaluation-agaration/), [Πολιτική απορρήτου](https://mattermost.com/privacy-policy/) και λήψη ενημερωτικών email." + }, + { + "id": "api.server.warn_metric.number_of_active_users_200.notification_title", + "translation": "Αναπτυχτείτε μαζί με το Mattermost" + }, + { + "id": "api.command_share.permission_required", + "translation": "Απαιτείται η άδεια `{{.Permission}}` για τη διαχείριση κοινόχρηστων καναλιών." + }, + { + "id": "api.command_share.not_shared_channel_unshare", + "translation": "Δεν είναι δυνατή η άρση της κοινής χρήσης ενός καναλιού που δεν είναι κοινόχρηστο." + }, + { + "id": "api.command_share.no_remote_invited", + "translation": "Δεν έχουν προσκληθεί ασφαλείς συνδέσεις σε αυτό το κανάλι." + }, + { + "id": "api.command_share.name", + "translation": "κοινή χρήση καναλιού" + }, + { + "id": "api.command_share.must_specify_valid_remote", + "translation": "Πρέπει να καθορίσετε ένα έγκυρο αναγνωριστικό ασφαλούς σύνδεσης για πρόσκληση." + }, + { + "id": "api.command_share.missing_action", + "translation": "Λείπει η ενέργεια. Διαθέσιμες ενέργειες: {{.Actions}}" + }, + { + "id": "api.command_share.invite_remote.help", + "translation": "Προσκαλεί έναν εξωτερικό διακομιστή Mattermost στο τρέχον κοινόχρηστο κανάλι" + }, + { + "id": "api.command_share.invitation_sent", + "translation": "Η πρόσκληση κοινόχρηστου καναλιού έχει σταλεί στο `{{.Όνομα}} {{.SiteURL}}`." + }, + { + "id": "api.command_share.invalid_value.error", + "translation": "Μη έγκυρη τιμή για το '{{.Arg}}': {{.Error}}" + }, + { + "id": "api.command_share.hint", + "translation": "[ενέργεια]" + }, + { + "id": "api.command_share.fetch_remote_status.error", + "translation": "Δεν ήταν δυνατή η ανάκτηση της κατάστασης ασφαλών συνδέσεων: {{.Error}}." + }, + { + "id": "api.command_share.fetch_remote.error", + "translation": "Σφάλμα ανάκτησης ασφαλών συνδέσεων: {{.Error}}" + }, + { + "id": "api.command_share.desc", + "translation": "Μοιράζεται το τρέχον κανάλι με έναν εξωτερικό διακομιστή Mattermost." + }, + { + "id": "api.command_share.could_not_uninvite.error", + "translation": "Δεν ήταν δυνατή η κατάργηση της πρόσκλησης `{{.RemoteId}}`: {{.Error}}" + }, + { + "id": "api.command_share.check_channel_exist.error", + "translation": "Σφάλμα κατά τον έλεγχο αν υπάρχει κοινόχρηστο κανάλι: {{.Error}}" + }, + { + "id": "api.command_share.channel_status_id", + "translation": "Κατάσταση για το αναγνωριστικό καναλιού `{{.ChannelId}}`" + }, + { + "id": "api.command_share.channel_status.help", + "translation": "Εμφανίζει την κατάσταση για αυτό το κοινόχρηστο κανάλι" + }, + { + "id": "api.command_share.channel_shared", + "translation": "Αυτό το κανάλι είναι πλέον κοινόχρηστο." + }, + { + "id": "api.command_share.channel_remote_id_not_exists", + "translation": "Δεν υπάρχει ασφαλής σύνδεση κοινόχρηστου καναλιού `{{.RemoteId}}` για αυτό το κανάλι." + }, + { + "id": "api.command_share.channel_invite_not_home.error", + "translation": "Δεν είναι δυνατή η πρόσκληση ασφαλούς σύνδεσης σε κοινόχρηστο κανάλι που προέρχεται κάπου αλλού." + }, + { + "id": "api.command_share.channel_invite.error", + "translation": "Σφάλμα κατά την πρόσκληση `{{.Name}}` σε αυτό το κανάλι: {{.Error}}" + }, + { + "id": "api.command_share.available_actions", + "translation": "Διαθέσιμες ενέργειες: {{.Actions}}" + }, + { + "id": "api.command_remove.group_constrained_user_denied", + "translation": "Ο χρήστης δεν μπορεί να αφαιρεθεί από το κανάλι από εσάς επειδή είναι μέλος των γκρουπ που συνδέονται με αυτό το κανάλι. Για να τους αφαιρέσετε από αυτό το κανάλι, πρέπει να τους αφαιρέσετε από τα συνδεδεμένα γκρουπ." + }, + { + "id": "api.command_remote.unknown_action", + "translation": "Άγνωστη ενέργεια `{{.Action}}`" + }, + { + "id": "api.command_remote.status.help", + "translation": "Εμφανίζει την κατάσταση όλων των ασφαλών συνδέσεων" + }, + { + "id": "api.command_remote.site_url_not_set", + "translation": "Η διεύθυνση URL του ιστότοπου δεν έχει οριστεί. Παρακαλούμε ορίστε τη στην Κονσόλα συστήματος > Περιβάλλον > Διακομιστής Web." + }, + { + "id": "api.command_remote.service_not_enabled", + "translation": "Η υπηρεσία ασφαλούς σύνδεσης δεν είναι ενεργοποιημένη." + }, + { + "id": "api.command_remote.service_disabled", + "translation": "Η υπηρεσία ασφαλούς σύνδεσης είναι απενεργοποιημένη." + }, + { + "id": "api.command_remote.remove_remote_id.help", + "translation": "Αναγνωριστικό της ασφαλούς σύνδεσης προς κατάργηση." + }, + { + "id": "api.command_remote.remove_remote.error", + "translation": "Δεν ήταν δυνατή η κατάργηση ασφαλούς σύνδεσης: {{.Error}}" + }, + { + "id": "api.command_remote.remove.help", + "translation": "Καταργεί μια ασφαλή σύνδεση" + }, + { + "id": "api.command_remote.remotes_not_found", + "translation": "Δε βρέθηκαν ασφαλείς συνδέσεις." + }, + { + "id": "api.command_remote.remote_table_header", + "translation": "| Ασφαλής σύνδεση | Εμφανιζόμενο όνομα | Αναγνωριστικό σύνδεσης | URL ιστότοπου | Πρόσκληση αποδεκτή | Σε σύνδεση | Τελευταίο ping |" + }, + { + "id": "api.command_remote.remote_add_remove.help", + "translation": "Προσθήκη/αφαίρεση ασφαλών συνδέσεων. Διαθέσιμες ενέργειες: {{.Actions}}" + }, + { + "id": "api.command_remote.permission_required", + "translation": "Για τη διαχείριση ασφαλών συνδέσεων απαιτείται η άδεια `{{.Permission}}`." + }, + { + "id": "api.command_remote.name.hint", + "translation": "Ένα μοναδικό όνομα για την ασφαλή σύνδεση" + }, + { + "id": "api.command_remote.name.help", + "translation": "Όνομα ασφαλούς σύνδεσης" + }, + { + "id": "api.command_remote.name", + "translation": "ασφαλής σύνδεση" + }, + { + "id": "api.command_remote.missing_empty", + "translation": "Λείπει ή είναι κενό το `{{.Arg}}`" + }, + { + "id": "api.command_remote.missing_command", + "translation": "Λείπει η εντολή. Διαθέσιμες ενέργειες: {{.Actions}}" + }, + { + "id": "api.command_remote.invite_summary", + "translation": "Στείλτε την ακόλουθη κρυπτογραφημένη με AES 256-bit πρόσκληση στον εξωτερικό διαχειριστή συστήματος Mattermost μαζί με τον κωδικό πρόσβασης. Αυτός θα χρησιμοποιήσει την εντολή `{{.Command}}` για να αποδεχτεί την πρόσκληση.\n\n```\n{{.Invitation}}\n```\n\n**Βεβαιωθείτε ότι η ασφαλής σύνδεση μπορεί να έχει πρόσβαση στο Mattermost διακομιστή σας μέσω** {{.SiteURL}}" + }, + { + "id": "api.command_remote.invite_password.hint", + "translation": "Κωδικός πρόσβασης που θα χρησιμοποιηθεί για την κρυπτογράφηση της πρόσκλησης" + }, + { + "id": "api.command_remote.invite_password.help", + "translation": "Κωδικός πρόσκλησης" + }, + { + "id": "api.command_remote.invite.help", + "translation": "Προσκαλέστε μια ασφαλή σύνδεση" + }, + { + "id": "api.command_remote.invitation_created", + "translation": "Η πρόσκληση δημιουργήθηκε." + }, + { + "id": "api.command_remote.invitation.hint", + "translation": "Η κρυπτογραφημένη πρόσκληση από ασφαλή σύνδεση" + }, + { + "id": "api.command_remote.invitation.help", + "translation": "Πρόσκληση από ασφαλή σύνδεση" + }, + { + "id": "api.command_remote.incorrect_password.error", + "translation": "Δεν ήταν δυνατή η αποκρυπτογράφηση πρόσκλησης. Εσφαλμένος κωδικός πρόσβασης ή κατεστραμμένη πρόσκληση: {{.Error}}" + }, + { + "id": "api.command_remote.hint", + "translation": "[ενέργεια]" + }, + { + "id": "api.command_remote.fetch_status.error", + "translation": "Δεν ήταν δυνατή η ανάκτηση ασφαλών συνδέσεων: {{.Error}}" + }, + { + "id": "api.command_remote.encrypt_invitation.error", + "translation": "Δεν ήταν δυνατή η κρυπτογράφηση της πρόσκλησης: {{.Error}}" + }, + { + "id": "api.command_remote.decode_invitation.error", + "translation": "Δεν ήταν δυνατή η αποκωδικοποίηση της πρόσκλησης: {{.Error}}" + }, + { + "id": "api.command_remote.accept_invitation.error", + "translation": "Δεν ήταν δυνατή η αποδοχή της πρόσκλησης: {{.Error}}" + }, + { + "id": "api.command_remote.displayname.hint", + "translation": "Ένα εμφανιζόμενο όνομα για την ασφαλή σύνδεση" + }, + { + "id": "api.command_remote.displayname.help", + "translation": "Εμφανιζόμενο όνομα ασφαλούς σύνδεσης" + }, + { + "id": "api.command_remote.desc", + "translation": "Προσκαλέστε για επικοινωνία με ασφαλείς συνδέσεις μεταξύ διακομιστών Mattermost." + }, + { + "id": "api.command_remote.cluster_removed", + "translation": "Ασφαλής σύνδεση {{.RemoteId}} {{.Result}}." + }, + { + "id": "api.command_remote.add_remote.error", + "translation": "Δεν ήταν δυνατή η προσθήκη ασφαλούς σύνδεσης: {{.Error}}" + }, + { + "id": "api.command_remote.accept_invitation", + "translation": "Η πρόσκληση έγινε δεκτή και επιβεβαιώθηκε.\nSiteURL: {{.SiteURL}}" + }, + { + "id": "api.command_remote.accept.help", + "translation": "Αποδοχή πρόσκλησης από έναν εξωτερικό διακομιστή Mattermost" + }, + { + "id": "api.team.add_members.user_denied", + "translation": "Αυτή η ομάδα διαχειρίζεται από γκρουπ. Αυτός ο χρήστης δεν είναι μέρος ενός γκρουπ το οποίο να αντιστοιχεί με αυτήν την ομάδα." + }, + { + "id": "api.command_invite.group_constrained_user_denied", + "translation": "Αυτή η ομάδα διαχειρίζεται από γκρουπ. Αυτός ο χρήστης δεν είναι μέρος ενός γκρουπ το οποίο να αντιστοιχεί με αυτό το κανάλι." + }, + { + "id": "api.command_expand.success", + "translation": "Οι σύνδεσμοι εικόνων αναπτύσσονται τώρα από προεπιλογή" + }, + { + "id": "api.command_echo.name", + "translation": "echo" + }, + { + "id": "api.command_echo.hint", + "translation": "'μήνυμα' [καθυστέρηση σε δευτερόλεπτα]" + }, + { + "id": "api.command_echo.high_volume.app_error", + "translation": "Υψηλός όγκος αιτημάτων echo, δεν είναι δυνατή η επεξεργασία του αιτήματος." + }, + { + "id": "api.command_echo.desc", + "translation": "Αντίγραφο κειμένου από τον λογαριασμό σας" + }, + { + "id": "api.command_collapse.success", + "translation": "Οι σύνδεσμοι εικόνων συμπτύσσονται τώρα από προεπιλογή" + }, + { + "id": "api.command_collapse.name", + "translation": "σύμπτυξη" + }, + { + "id": "api.command_channel_purpose.update_channel.max_length", + "translation": "Το κείμενο που έχει εισαχθεί υπερβαίνει το όριο χαρακτήρων. Ο σκοπός του καναλιού περιορίζεται σε {{.MaxLength}} χαρακτήρες." + }, + { + "id": "api.command_channel_purpose.direct_group.app_error", + "translation": "Δεν είναι δυνατός ο ορισμός σκοπού για τα κανάλια άμεσων μηνυμάτων. Χρησιμοποιήστε την εντολή '/header' για να ορίσετε την επικεφαλίδα." + }, + { + "id": "api.command_channel_purpose.desc", + "translation": "Επεξεργασία του σκοπού του καναλιού" + }, + { + "id": "api.command_channel_header.update_channel.max_length", + "translation": "Το κείμενο που έχει εισαχθεί υπερβαίνει το όριο χαρακτήρων. Η επικεφαλίδα του καναλιού περιορίζεται σε {{.MaxLength}} χαρακτήρες." + }, + { + "id": "api.cloud.request_error", + "translation": "Σφάλμα επεξεργασίας αιτήματος προς το CWS." + }, + { + "id": "api.cloud.license_error", + "translation": "Η άδειά σας δεν υποστηρίζει αιτήματα cloud." + }, + { + "id": "api.cloud.cws_webhook_event_missing_error", + "translation": "Το συμβάν Webhook δε διεκπεραιώθηκε. Είτε λείπει είτε δεν είναι έγκυρο." + }, + { + "id": "api.cloud.app_error", + "translation": "Εσωτερικό σφάλμα κατά τη διάρκεια αιτήματος του cloud api." + }, + { + "id": "api.channel.update_team_member_roles.changing_guest_role.app_error", + "translation": "Μη έγκυρη ενημέρωση μέλους ομάδας: Δεν μπορείτε να προσθέσετε ή να καταργήσετε τον ρόλο επισκέπτη χειροκίνητα." + }, + { + "id": "api.channel.update_channel_privacy.default_channel_error", + "translation": "Το προεπιλεγμένο κανάλι δεν μπορεί να γίνει ιδιωτικό." + }, + { + "id": "api.channel.update_channel_member_roles.guest_and_user.app_error", + "translation": "Μη έγκυρη ενημέρωση μέλους καναλιού: Ένας χρήστης πρέπει να είναι είτε επισκέπτης είτε χρήστης, αλλά όχι και τα δύο." + }, + { + "id": "api.channel.update_channel_member_roles.changing_guest_role.app_error", + "translation": "Μη έγκυρη ενημέρωση μέλους καναλιού: Δεν μπορείτε να προσθέσετε ή να καταργήσετε τον ρόλο επισκέπτη χειροκίνητα." + }, + { + "id": "api.channel.update_channel.typechange.app_error", + "translation": "Αυτός ο τύπος καναλιού δεν μπορεί να ενημερωθεί." + }, + { + "id": "api.channel.restore_channel.unarchived", + "translation": "Ο {{.Username}} κατάργησε την αρχειοθέτηση του καναλιού." + }, + { + "id": "api.channel.restore_channel.restored.app_error", + "translation": "Αδύνατη η κατάργηση της αρχειοθέτησης του καναλιού. Το κανάλι δεν έχει αρχειοθετηθεί." + }, + { + "id": "api.channel.add_members.user_denied", + "translation": "Η συμμετοχή στο κανάλι δεν επιτρέπεται στους ακόλουθους χρήστες λόγω περιορισμών γκρουπ: {{ .UserIDs }}" + }, + { + "id": "api.channel.remove_members.denied", + "translation": "Η κατάργηση συμμετοχής στο κανάλι δεν επιτρέπεται στους ακόλουθους χρήστες λόγω περιορισμών γκρουπ: {{ .UserIDs }}" + }, + { + "id": "api.channel.remove_member.group_constrained.app_error", + "translation": "Δεν είναι δυνατή η κατάργηση ενός χρήστη από ένα κανάλι περιορισμένου γκρουπ." + }, + { + "id": "api.channel.patch_channel_moderations.license.error", + "translation": "Η άδειά σας δεν υποστηρίζει τη διαχείριση καναλιών" + }, + { + "id": "api.channel.patch_channel_moderations.cache_invalidation.error", + "translation": "Σφάλμα κατά την ακύρωση της προσωρινής μνήμης cache" + }, + { + "id": "api.channel.move_channel.type.invalid", + "translation": "Δεν είναι δυνατή η μετακίνηση άμεσων ή ομαδικών μηνυμάτων σε κανάλια" + }, + { + "id": "api.channel.guest_join_channel.post_and_forget", + "translation": "%v μπήκε στο κανάλι ως επισκέπτης." + }, + { + "id": "api.channel.get_channel_moderations.license.error", + "translation": "Η άδειά σας δεν υποστηρίζει τη διαχείριση καναλιών" + }, + { + "id": "api.channel.channel_member_counts_by_group.license.error", + "translation": "Η άδειά σας δεν υποστηρίζει γκρουπ" + }, + { + "id": "api.channel.add_members.error", + "translation": "Σφάλμα προσθήκης μέλους(ων) καναλιού." + }, + { + "id": "api.bot.teams_channels.add_message_mobile", + "translation": "Παρακαλώ προσθέστε με σε ομάδες και κανάλια που θέλετε να συμμετέχω. Για να το κάνετε αυτό, χρησιμοποιήστε το πρόγραμμα περιήγησης ή την εφαρμογή του Mattermost Desktop." + }, + { + "id": "api.bot.create_disabled", + "translation": "Η δημιουργία bot έχει απενεργοποιηθεί." + }, + { + "id": "api.admin.saml.set_certificate_from_metadata.missing_content_type.app_error", + "translation": "Λείπει ο τύπος περιεχομένου." + }, + { + "id": "api.admin.saml.set_certificate_from_metadata.invalid_content_type.app_error", + "translation": "Μη έγκυρος τύπος περιεχομένου." + }, + { + "id": "api.admin.saml.set_certificate_from_metadata.invalid_body.app_error", + "translation": "Μη έγκυρο κείμενο πιστοποιητικού." + }, + { + "id": "api.admin.saml.invalid_xml_missing_idpssodescriptors.app_error", + "translation": "Λείπει ο κόμβος παρόχου ταυτότητας SSO Descriptors στην XML." + }, + { + "id": "api.admin.saml.invalid_xml_missing_ssoservices.app_error", + "translation": "Λείπει ο κόμβος υπηρεσιών του παρόχου ταυτότητας SSO στην XML." + }, + { + "id": "api.admin.saml.invalid_xml_missing_keydescriptor.app_error", + "translation": "Λείπει ο κόμβος Identity Provider Key Descriptors στην XML." + }, + { + "id": "api.admin.saml.failure_reset_authdata_to_email.app_error", + "translation": "Απέτυχε η επαναφορά του πεδίου AuthData σε Email." + }, + { + "id": "api.admin.saml.failure_parse_idp_certificate.app_error", + "translation": "Παρουσιάστηκε αποτυχία κατά την ανάλυση των πληροφοριών μεταδεδομένων που ελήφθησαν από τον πάροχο ταυτότητας σε ένα πιστοποιητικό." + }, + { + "id": "api.admin.saml.failure_get_metadata_from_idp.app_error", + "translation": "Απέτυχε η λήψη μεταδεδομένων από τη διεύθυνση ιστοσελίδας του παρόχου ταυτότητας." + }, + { + "id": "api.admin.ldap.not_available.app_error", + "translation": "Το LDAP δεν είναι διαθέσιμο." + }, + { + "id": "api.admin.delete_brand_image.storage.not_found", + "translation": "Αδυναμία διαγραφής εικόνας μάρκας, δε βρέθηκε." + }, + { + "id": "api.admin.add_certificate.parseform.app_error", + "translation": "Σφάλμα ανάλυσης πολυμορφικής αίτησης" + }, + { + "id": "api.post.search_posts.invalid_body.app_error", + "translation": "Δεν είναι δυνατή η ανάλυση του αιτήματος." + }, + { + "id": "api.post.search_files.invalid_body.app_error", + "translation": "Δεν είναι δυνατή η ανάλυση του αιτήματος." + }, + { + "id": "api.team.invalidate_all_email_invites.app_error", + "translation": "Σφάλμα κατά την ακύρωση προσκλήσεων email." + }, + { + "id": "api.team.import_team.unknown_import_from.app_error", + "translation": "Άγνωστη πηγή εισαγωγής." + }, + { + "id": "api.team.import_team.unavailable.app_error", + "translation": "Αίτημα με λανθασμένη μορφή: Απουσιάζει το πεδίο 'μέγεθος αρχείου' (filesize)." + }, + { + "id": "api.team.import_team.no_import_from.app_error", + "translation": "Αίτημα με λανθασμένη μορφή: Απουσιάζει το πεδίο 'εισαγωγή από' (importFrom)." + }, + { + "id": "api.team.import_team.integer.app_error", + "translation": "Το μέγεθος αρχείου (filesize) δεν είναι ακέραιος αριθμός." + }, + { + "id": "api.team.get_team_icon.read_file.app_error", + "translation": "Δεν είναι δυνατή η ανάγνωση του αρχείου εικονιδίου της ομάδας." + }, + { + "id": "api.team.get_team_icon.filesettings_no_driver.app_error", + "translation": "Μη έγκυρο όνομα προγράμματος οδήγησης για τις ρυθμίσεις αρχείου. Πρέπει να είναι είτε 'local' ή 'amazons3'." + }, + { + "id": "api.team.get_invite_info.not_open_team", + "translation": "Η πρόσκληση δεν είναι έγκυρη επειδή αυτή δεν είναι μια ανοιχτή ομάδα." + }, + { + "id": "api.slackimport.slack_import.note1", + "translation": "- Ορισμένα μηνύματα ενδέχεται να μην έχουν εισαχθεί επειδή δεν υποστηρίζονται από αυτόν τον συγκεκριμένο τύπο εισαγωγέα.\n" + }, + { + "id": "api.slackimport.slack_import.log", + "translation": "Εισαγωγή αρχείου καταγραφής Mattermost Slack\n" + }, + { + "id": "api.slackimport.slack_add_users.unable_import", + "translation": "Δεν είναι δυνατή η εισαγωγή του χρήστη Slack: {{.Username}}.\n" + }, + { + "id": "api.slackimport.slack_add_users.missing_email_address", + "translation": "Ο χρήστης {{.Username}} δεν είχε διεύθυνση email κατά την εξαγωγή από το Slack. Χρησιμοποιήθηκε το {{.Email}} ως προσωρινό. Ο χρήστης θα πρέπει να ενημερώσει τη διεύθυνση email του μόλις συνδεθεί στο σύστημα.\n" + }, + { + "id": "api.slackimport.slack_add_users.merge_existing", + "translation": "Ο χρήστης Slack συγχωνεύτηκε με έναν υπάρχοντα χρήστη Mattermost με email {{.Email}} και όνομα χρήστη {{.Username}}.\n" + }, + { + "id": "api.slackimport.slack_add_users.merge_existing_failed", + "translation": "Ο χρήστης Slack συγχωνεύτηκε με έναν υπάρχοντα χρήστη Mattermost με email {{.Email}} και όνομα χρήστη {{.Username}}, αλλά δεν μπόρεσε να προσθέσει τον χρήστη στην ομάδα του.\n" + }, + { + "id": "api.slackimport.slack_add_users.email_pwd", + "translation": "Έγινε εισαγωγή του χρήστη Slack με email {{.Email}} και κωδικό πρόσβασης {{.Password}}.\n" + }, + { + "id": "api.slackimport.slack_add_users.created", + "translation": "\nΟι χρήστες δημιούργησαν:\n" + }, + { + "id": "api.slackimport.slack_add_channels.merge", + "translation": "Το κανάλι Slack {{.DisplayName}} υπάρχει ήδη ως ενεργό κανάλι στο Mattermost. Και τα δύο κανάλια συγχωνεύτηκαν.\n" + }, + { + "id": "api.slackimport.slack_add_channels.import_failed", + "translation": "Δεν είναι δυνατή η εισαγωγή καναλιού Slack {{.DisplayName}}.\n" + }, + { + "id": "api.slackimport.slack_add_channels.failed_to_add_user", + "translation": "Δεν είναι δυνατή η προσθήκη του χρήστη Slack {{.Username}} στο κανάλι.\n" + }, + { + "id": "api.slackimport.slack_add_channels.added", + "translation": "\nΠροστέθηκαν τα κανάλια:\n" + }, + { + "id": "api.slackimport.slack_add_bot_user.unable_import", + "translation": "Δεν είναι δυνατή η εισαγωγή του χρήστη Integration/Slack Bot {{.Username}}.\n" + }, + { + "id": "api.slackimport.slack_add_bot_user.email_pwd", + "translation": "Έγινε εισαγωγή του χρήστη Integration/Slack Bot με email {{.Email}} και κωδικό πρόσβασης {{.Password}}.\n" + }, + { + "id": "api.server.warn_metric.number_of_teams_5.start_trial_notification_success.message", + "translation": "Η Εταιρική σας δοκιμαστική έκδοση είναι πλέον ενεργή. Μεταβείτε στην ενότητα **Κονσόλα συστήματος > Διαχείριση χρηστών > Δικαιώματα** για να ενεργοποιήσετε τα Προηγμένα δικαιώματα." + }, + { + "id": "api.server.warn_metric.number_of_teams_5.start_trial.notification_body", + "translation": "Το Mattermost σας έχει τώρα αρκετές ομάδες. Πολλές ομάδες έχουν τον δικό τους τρόπο συντονισμού και συνεργασίας, συμπεριλαμβανομένου και του τρόπου δημιουργίας καναλιών, τα οποία μπορούν να προσκαλέσουν νέους συναδέλφους καθώς και του τρόπου που διαχειρίζονται τα integrations. Τα Σχέδια Παράκαμψης Ομάδας σας επιτρέπουν να προσαρμόσετε τα δικαιώματα χρήστη σε κάθε ομάδα για να ικανοποιήσετε τις συγκεκριμένες ανάγκες τους.\n\n[Μάθετε περισσότερα σχετικά με τη χρήση των Προηγμένων δικαιωμάτων] (https://www.mattermost.com/docs-advanced-permissions-team-override/?utm_medium=product&utm_source=mattermost-advisor-bot&utm_content=advanced-permissions-team-override).\n\nΕπιλέγοντας την Έναρξη δοκιμαστικής έκδοσης, συμφωνώ με τις [Συμφωνία Αξιολόγησης Λογισμικού Mattermost](https://mattermost.com/software-evaluation-agaration/), [Πολιτική απορρήτου](https://mattermost.com/privacy-policy/) και λήψη ενημερωτικών email." + }, + { + "id": "api.server.warn_metric.number_of_teams_5.notification_title", + "translation": "Χρήση Προηγμένων Δικαιωμάτων" + }, + { + "id": "api.server.warn_metric.number_of_teams_5.notification_body", + "translation": "Το Mattermost σας έχει τώρα αρκετές ομάδες. Πολλές ομάδες έχουν τον δικό τους τρόπο συντονισμού και συνεργασίας, συμπεριλαμβανομένου και του τρόπου δημιουργίας καναλιών, τα οποία μπορούν να προσκαλέσουν νέους συναδέλφους καθώς και του τρόπου που διαχειρίζονται τα integrations. Τα Σχέδια Παράκαμψης Ομάδας σας επιτρέπουν να προσαρμόσετε τα δικαιώματα χρήστη σε κάθε ομάδα για να ικανοποιήσετε τις συγκεκριμένες ανάγκες τους.\n\n[Μάθετε περισσότερα σχετικά με τη χρήση των Προηγμένων δικαιωμάτων] (https://www.mattermost.com/docs-advanced-permissions-team-override/?utm_medium=product&utm_source=mattermost-advisor-bot&utm_content=advanced-permissions-team-override).\n\nΚάνοντας κλικ στην επιλογή Επικοινωνήστε μαζί μας, θα μοιραστείτε τις πληροφορίες σας με τη Mattermost, Inc. [Μάθετε περισσότερα] (https://mattermost.com/pl/default-admin-advisory)" + }, + { + "id": "api.server.warn_metric.number_of_teams_5.contact_us.email_body", + "translation": "Αίτημα επικοινωνίας με τη Mattermost. Ενδιαφέρομαι να μάθω περισσότερα σχετικά με τη χρήση των προηγμένων δικαιωμάτων με σχήματα συστήματος.\n" + }, + { + "id": "api.server.warn_metric.number_of_posts_2M.start_trial.notification_success.message", + "translation": "Η Εταιρική σας δοκιμαστική έκδοση είναι πλέον ενεργή. Μόλις έχετε το Elasticsearch, μεταβείτε στο **Κονσόλα συστήματος > Περιβάλλον > Elasticsearch** για να διαμορφώσετε το Elasticsearch." + }, + { + "id": "api.server.warn_metric.number_of_posts_2M.start_trial.notification_body", + "translation": "Το Mattermost σας διαθέτει μεγάλο αριθμό μηνυμάτων. Η προεπιλεγμένη αναζήτηση βάσης δεδομένων του Mattermost αρχίζει να δείχνει σημάδια υποβάθμισης της απόδοσης του, σε περίπου 2,5 εκατομμύρια δημοσιεύσεις. Με πάνω από 5 εκατομμύρια δημοσιεύσεις, το Elasticsearch μπορεί να σας βοηθήσει στο να αποφύγετε σημαντικά ζητήματα απόδοσης, όπως χρονικές διακοπές στις αναζητήσεις και στην εύρεση αναφορών. Επικοινωνήστε μαζί μας για να μάθετε περισσότερα και πείτε μας πώς μπορούμε να βοηθήσουμε.\n\n[Μάθετε περισσότερα σχετικά με τη βελτίωση της απόδοσης] (https://www.mattermost.com/docs-elasticsearch/?utm_medium=product&utm_source=mattermost-advisor-bot&utm_content=elasticsearch)\n\nΕπιλέγοντας την Έναρξη δοκιμαστικής έκδοσης, συμφωνώ με τις [Συμφωνία Αξιολόγησης Λογισμικού Mattermost](https://mattermost.com/software-evaluation-agaration/), [Πολιτική απορρήτου](https://mattermost.com/privacy-policy/) και λήψη ενημερωτικών email." + }, + { + "id": "api.server.warn_metric.number_of_posts_2M.notification_title", + "translation": "Βελτίωση της απόδοσης" + }, + { + "id": "api.server.warn_metric.number_of_posts_2M.notification_body", + "translation": "Το Mattermost σας διαθέτει μεγάλο αριθμό μηνυμάτων. Η προεπιλεγμένη αναζήτηση βάσης δεδομένων του Mattermost αρχίζει να δείχνει σημάδια υποβάθμισης της απόδοσης του, σε περίπου 2,5 εκατομμύρια δημοσιεύσεις. Με πάνω από 5 εκατομμύρια δημοσιεύσεις, το Elasticsearch μπορεί να σας βοηθήσει στο να αποφύγετε σημαντικά ζητήματα απόδοσης, όπως χρονικές διακοπές στις αναζητήσεις και στην εύρεση αναφορών. Επικοινωνήστε μαζί μας για να μάθετε περισσότερα και πείτε μας πώς μπορούμε να βοηθήσουμε.\n\n[Μάθετε περισσότερα σχετικά με τη βελτίωση της απόδοσης] (https://www.mattermost.com/docs-elasticsearch/?utm_medium=product&utm_source=mattermost-advisor-bot&utm_content=elasticsearch)\n\nΚάνοντας κλικ στην επιλογή Επικοινωνήστε μαζί μας, θα μοιραστείτε τις πληροφορίες σας με τη Mattermost, Inc. [Μάθετε περισσότερα] (https://mattermost.com/pl/default-admin-advisory)" + }, + { + "id": "api.server.warn_metric.number_of_posts_2M.contact_us.email_body", + "translation": "Αίτημα επικοινωνίας με τη Mattermost. Ενδιαφέρομαι να μάθω περισσότερα σχετικά με τη βελτίωση της απόδοσης με το Elasticsearch.\n" + }, + { + "id": "api.server.warn_metric.number_of_channels_50.start_trial.notification_success.message", + "translation": "Η Εταιρική σας δοκιμαστική έκδοση είναι πλέον ενεργή. Μεταβείτε στην ενότητα **Κονσόλα συστήματος > Διαχείριση χρηστών > Δικαιώματα** για να ενεργοποιήσετε τα Προηγμένα δικαιώματα." + }, + { + "id": "api.server.warn_metric.number_of_channels_50.start_trial.notification_body", + "translation": "Τα κανάλια συμβάλλουν στη βελτίωση της επικοινωνίας, αλλά με τους χρήστες στο Mattermost να συμμετέχουν και να δημιουργούν κανάλια, η πρόκληση να διατηρηθεί το σύστημα οργανωμένο αυξάνεται. Τα Προηγμένα δικαιώματα σάς δίνουν τη δυνατότητα να ορίσετε ποιοι χρήστες ή ρόλοι μπορούν να εκτελέσουν συγκεκριμένες ενέργειες, συμπεριλαμβανομένης της διαχείρισης ρυθμίσεων καναλιού και μελών, χρησιμοποιώντας το @channel ή το @here για να επισημάνετε ευρείες ομάδες χρηστών και τη δημιουργία νέων webhooks.\n\n[Μάθετε περισσότερα σχετικά με τη χρήση Προηγμένων δικαιωμάτων](https://www.mattermost.com/docs-advanced-permissions/?utm_medium=product&utm_source=mattermost-advisor-bot&utm_content=advanced-permissions)\n\nΕπιλέγοντας την Έναρξη δοκιμαστικής έκδοσης, συμφωνώ με τις [Συμφωνία Αξιολόγησης Λογισμικού Mattermost](https://mattermost.com/software-evaluation-agaration/), [Πολιτική απορρήτου](https://mattermost.com/privacy-policy/) και λήψη ενημερωτικών email." + }, + { + "id": "api.team.demote_user_to_guest.license.error", + "translation": "Η άδειά σας δεν υποστηρίζει λογαριασμούς επισκεπτών" + }, + { + "id": "api.team.demote_user_to_guest.disabled.error", + "translation": "Οι λογαριασμοί επισκεπτών είναι απενεργοποιημένοι." + }, + { + "id": "api.team.add_user_to_team_from_invite.guest.app_error", + "translation": "Οι επισκέπτες δεν επιτρέπεται να συμμετέχουν σε μια ομάδα μέσω ενός συνδέσμου πρόσκλησης. Ζητήστε από την ομάδα να σας στείλει μέσω email μια πρόσκληση επισκεπτών." + }, + { + "id": "api.team.add_user_to_team.missing_parameter.app_error", + "translation": "Απαιτείται παράμετρος για την προσθήκη του χρήστη στην ομάδα." + }, + { + "id": "api.team.add_team_member.invalid_body.app_error", + "translation": "Δεν είναι δυνατή η ανάλυση του αιτήματος." + }, + { + "id": "api.team.add_members.error", + "translation": "Σφάλμα κατά την προσθήκη μελών στην ομάδα." + }, + { + "id": "api.system.update_viewed_notices.failed", + "translation": "Η ενημέρωση των ειδοποιήσεων που προβλήθηκαν απέτυχε" + }, + { + "id": "api.system.update_notices.validating_failed", + "translation": "Η επικύρωση των συνθηκών ειδοποίησης προϊόντος απέτυχε" + }, + { + "id": "api.system.update_notices.parse_failed", + "translation": "Η ανάλυση των ειδοποιήσεων προϊόντος απέτυχε" + }, + { + "id": "api.system.update_notices.fetch_failed", + "translation": "Η λήψη ειδοποιήσεων προϊόντος απέτυχε" + }, + { + "id": "api.system.update_notices.clear_failed", + "translation": "Η διαγραφή παλιών ειδοποιήσεων προϊόντος απέτυχε" + }, + { + "id": "api.system.id_loaded.not_available.app_error", + "translation": "Οι ειδοποιήσεις Push Loaded ID δεν έχουν ρυθμιστεί ή δεν υποστηρίζονται σε αυτόν τον διακομιστή." + }, + { + "id": "api.status.user_not_found.app_error", + "translation": "Ο χρήστης δεν βρέθηκε." + }, + { + "id": "api.slackimport.slack_import.zip.file_too_large", + "translation": "Το αρχείο zip {{.Filename}} είναι πολύ μεγάλο για να γίνει εισαγωγή από Slack\n" + }, + { + "id": "api.slackimport.slack_import.zip.app_error", + "translation": "Δεν είναι δυνατό το άνοιγμα του αρχείου εξαγωγής zip από το Slack.\n" + }, + { + "id": "api.slackimport.slack_import.team_fail", + "translation": "Δεν είναι δυνατή η εισαγωγή της ομάδας.\n" + }, + { + "id": "api.slackimport.slack_import.open.app_error", + "translation": "Δεν είναι δυνατό το άνοιγμα του αρχείου: {{.Filename}}.\n" + }, + { + "id": "api.slackimport.slack_import.notes", + "translation": "\nΣημειώσεις:\n" + }, + { + "id": "api.slackimport.slack_import.note3", + "translation": "- Πρόσθετα σφάλματα ενδέχεται να βρεθούν στα αρχεία καταγραφής του διακομιστή.\n" + }, + { + "id": "api.slackimport.slack_import.note2", + "translation": "- Τα μηνύματα του Slack bot δεν υποστηρίζονται προς το παρόν.\n" + }, + { + "id": "api.command_share.remote_already_invited", + "translation": "Η ασφαλής σύνδεση έχει ήδη προσκληθεί." + }, + { + "id": "api.email_batching.add_notification_email_to_batch.channel_full.app_error", + "translation": "Το κανάλι λήψης των ομαδικών μηνυμάτων ηλεκτρονικού ταχυδρομείου ήταν πλήρες. Παρακαλούμε αυξήστε το EmailBatchingBufferSize." + }, + { + "id": "api.email.send_warn_metric_ack.missing_server.app_error", + "translation": "Απαιτείται διακομιστής SMTP" + }, + { + "id": "api.email.send_warn_metric_ack.invalid_warn_metric.app_error", + "translation": "Δεν ήταν δυνατή η εύρεση της μέτρησης της προειδοποίησης." + }, + { + "id": "api.email.send_warn_metric_ack.failure.app_error", + "translation": "Αποτυχία αποστολής email επιβεβαίωσης διαχειριστή" + }, + { + "id": "api.create_terms_of_service.empty_text.app_error", + "translation": "Παρακαλούμε εισάγετε κείμενο για τους Προσαρμοσμένους Όρους Παροχής Υπηρεσιών." + }, + { + "id": "api.create_terms_of_service.custom_terms_of_service_disabled.app_error", + "translation": "Η δυνατότητα προσαρμοσμένων όρων υπηρεσίας είναι απενεργοποιημένη." + }, + { + "id": "api.context.token_provided.app_error", + "translation": "Η περίοδος σύνδεσης δεν είναι OAuth, αλλά το διακριτικό δόθηκε στη συμβολοσειρά ερωτήματος." + }, + { + "id": "api.context.server_busy.app_error", + "translation": "Ο διακομιστής είναι απασχολημένος, οι μη κρίσιμες υπηρεσίες δεν είναι προσωρινά διαθέσιμες." + }, + { + "id": "api.context.remote_id_missing.app_error", + "translation": "Απουσία αναγνωριστικού ασφαλούς σύνδεσης." + }, + { + "id": "api.context.remote_id_mismatch.app_error", + "translation": "Αναντιστοιχία αναγνωριστικού ασφαλούς σύνδεσης." + }, + { + "id": "api.context.remote_id_invalid.app_error", + "translation": "Αδυναμία εύρεσης ασφαλούς σύνδεσης {{.RemoteId}}." + }, + { + "id": "api.context.local_origin_required.app_error", + "translation": "Αυτό το τελικό σημείο απαιτεί μια τοπική προέλευση αιτήματος." + }, + { + "id": "api.context.json_encoding.app_error", + "translation": "Σφάλμα κατά την κωδικοποίηση JSON." + }, + { + "id": "api.context.invitation_expired.error", + "translation": "Η πρόσκληση έχει λήξει." + }, + { + "id": "api.context.invalid_url_param.app_error", + "translation": "Μη έγκυρη ή ελλιπής παράμετρος {{.Name}} στη διεύθυνση URL αίτησης." + }, + { + "id": "api.context.invalid_token.error", + "translation": "Μη έγκυρο διακριτικό περιόδου σύνδεσης={{.Token}}, σφάλμα={{.Error}}" + }, + { + "id": "api.context.get_user.app_error", + "translation": "Δεν είναι δυνατή η λήψη χρήστη από τη συνεδρία UserID." + }, + { + "id": "api.config.update_config.diff.app_error", + "translation": "Αποτυχία διαμόρφωσης των diff configs" + }, + { + "id": "api.config.update_config.clear_siteurl.app_error", + "translation": "Δεν είναι δυνατή η εκκαθάριση της διεύθυνσης URL ιστότοπου." + }, + { + "id": "api.config.patch_config.restricted_merge.app_error", + "translation": "Απέτυχε η συγχώνευση της ρύθμισης που έχει δοθεί." + }, + { + "id": "api.config.patch_config.diff.app_error", + "translation": "Αποτυχία διαμόρφωσης των diff configs" + }, + { + "id": "api.config.migrate_config.app_error", + "translation": "Αποτυχία μετεγκατάστασης του χώρου αποθήκευσης ρυθμίσεων (config store)." + }, + { + "id": "api.config.get_config.restricted_merge.app_error", + "translation": "Απέτυχε η συγχώνευση της ρύθμισης που έχει δοθεί." + }, + { + "id": "api.config.client.old_format.app_error", + "translation": "Η νέα μορφή για τη διαμόρφωση του πελάτη δεν υποστηρίζεται ακόμη. Προσδιορίστε format=old στη συμβολοσειρά ερωτήματος." + }, + { + "id": "api.command_share.unshare_channel.help", + "translation": "Καταργεί την κοινή χρήση του τρέχοντος καναλιού" + }, + { + "id": "api.command_share.unknown_action", + "translation": "Άγνωστη ενέργεια `{{.Action}}`. Διαθέσιμες ενέργειες: {{.Actions}}" + }, + { + "id": "api.command_share.uninvite_remote_id.help", + "translation": "Αναγνωριστικό ασφαλούς σύνδεσης για την κατάργηση της πρόσκλησης." + }, + { + "id": "api.command_share.uninvite_remote.help", + "translation": "Ακύρωση πρόσκλησης μια ασφαλούς σύνδεσης από αυτό το κοινόχρηστο κανάλι" + }, + { + "id": "api.command_share.shared_channel_unshare.error", + "translation": "Δεν είναι δυνατή η ακύρωση κοινής χρήσης αυτού του καναλιού: {{.Error}}." + }, + { + "id": "api.command_share.shared_channel_unavailable", + "translation": "Αυτό το κανάλι δεν είναι πλέον κοινόχρηστο." + }, + { + "id": "api.command_share.share_read_only.hint", + "translation": "[μόνο για ανάγνωση] - 'Ν' ή 'Ο'. Προεπιλογή 'Ο'" + }, + { + "id": "api.command_share.share_read_only.help", + "translation": "Το κανάλι θα κοινοποιηθεί σε λειτουργία μόνο για ανάγνωση" + }, + { + "id": "api.command_share.share_channel.error", + "translation": "Δεν είναι δυνατή η κοινή χρήση αυτού του καναλιού: {{.Error}}" + }, + { + "id": "api.command_share.service_disabled", + "translation": "Η υπηρεσία κοινόχρηστων καναλιών είναι απενεργοποιημένη." + }, + { + "id": "api.command_share.remote_uninvited", + "translation": "Ασφαλής σύνδεση `{{.RemoteId}}` χωρίς πρόσκληση." + }, + { + "id": "api.command_share.remote_table_header", + "translation": "| Ασφαλής σύνδεση | URL του ιστότοπου | Μόνο για ανάγνωση | Πρόσκληση αποδεκτή | Online | Τελευταίος συγχρονισμός |" + }, + { + "id": "api.command_share.remote_not_valid", + "translation": "Πρέπει να καθορίσετε ένα έγκυρο αναγνωριστικό ασφαλούς σύνδεσης για να ακυρώσετε την πρόσκληση" + }, + { + "id": "api.command_share.remote_id_invalid.error", + "translation": "Το αναγνωριστικό ασφαλούς σύνδεσης δεν είναι έγκυρο: {{.Error}}" + }, + { + "id": "api.command_share.remote_id.help", + "translation": "Αναγνωριστικό μιας υπάρχουσας ασφαλούς σύνδεσης. Ανατρέξτε στην εντολή `secure-connection` για να προσθέσετε μια ασφαλή σύνδεση." + }, + { + "id": "api.channel.create_channel.direct_channel.team_restricted_error", + "translation": "Δεν είναι δυνατή η δημιουργία ενός άμεσου καναλιού μεταξύ αυτών των χρηστών, επειδή δε μοιράζονται μια κοινή ομάδα." + }, + { + "id": "api.error_get_first_admin_visit_marketplace_status", + "translation": "Σφάλμα κατά την ανάκτηση της κατάστασης της πρώτης επίσκεψης του διαχειριστή στο marketplace." + }, + { + "id": "api.emoji.upload.open.app_error", + "translation": "Δεν είναι δυνατή η δημιουργία του emoji. Παρουσιάστηκε σφάλμα κατά την προσπάθεια ανοίγματος της συνημμένης εικόνας." + }, + { + "id": "api.emoji.upload.large_image.too_large.app_error", + "translation": "Δεν είναι δυνατή η δημιουργία emoji. Η εικόνα πρέπει να είναι μικρότερη από {{.MaxWidth}} επί {{.MaxHeight}}." + }, + { + "id": "api.emoji.upload.large_image.gif_encode_error", + "translation": "Δεν είναι δυνατή η δημιουργία emoji. Παρουσιάστηκε σφάλμα κατά την προσπάθεια κωδικοποίησης της εικόνας GIF." + }, + { + "id": "api.emoji.upload.large_image.gif_decode_error", + "translation": "Δεν είναι δυνατή η δημιουργία emoji. Παρουσιάστηκε σφάλμα κατά την προσπάθεια αποκωδικοποίησης της εικόνας GIF." + }, + { + "id": "api.emoji.upload.large_image.encode_error", + "translation": "Δεν είναι δυνατή η δημιουργία emoji. Παρουσιάστηκε σφάλμα κατά την προσπάθεια κωδικοποίησης της εικόνας." + }, + { + "id": "api.emoji.upload.large_image.decode_error", + "translation": "Δεν είναι δυνατή η δημιουργία emoji. Παρουσιάστηκε σφάλμα κατά την προσπάθεια αποκωδικοποίησης της εικόνας." + }, + { + "id": "api.emoji.upload.image.app_error", + "translation": "Δεν είναι δυνατή η δημιουργία emoji. Το αρχείο πρέπει να είναι PNG, JPEG ή GIF." + }, + { + "id": "api.emoji.storage.app_error", + "translation": "Ο χώρος αποθήκευσης αρχείων δεν έχει ρυθμιστεί σωστά. Παρακαλούμε ρυθμίστε τις παραμέτρους είτε για αποθήκευση αρχείων στο S3 είτε σε τοπικό διακομιστή." + }, + { + "id": "api.emoji.get_image.read.app_error", + "translation": "Δεν είναι δυνατή η ανάγνωση του αρχείου εικόνας για το emoji." + }, + { + "id": "api.emoji.get_image.decode.app_error", + "translation": "Δεν είναι δυνατή η αποκωδικοποίηση του αρχείου εικόνας για emoji." + }, + { + "id": "api.emoji.create.too_large.app_error", + "translation": "Δεν είναι δυνατή η δημιουργία emoji. Η εικόνα πρέπει να έχει μέγεθος μικρότερο από 1 MB." + }, + { + "id": "api.emoji.create.parse.app_error", + "translation": "Δεν είναι δυνατή η δημιουργία emoji. Δεν ήταν δυνατή η κατανόηση του αιτήματος." + }, + { + "id": "api.emoji.create.other_user.app_error", + "translation": "Μη έγκυρο αναγνωριστικό χρήστη." + }, + { + "id": "api.emoji.create.internal_error", + "translation": "server_error: Παρουσιάστηκε σφάλμα εσωτερικού διακομιστή κατά τη δημιουργία του emoji." + }, + { + "id": "api.emoji.create.duplicate.app_error", + "translation": "Δεν είναι δυνατή η δημιουργία emoji. Υπάρχει ήδη ένα άλλο emoji με το ίδιο όνομα." + }, + { + "id": "api.email_batching.send_batched_email_notification.title", + "translation": { + "one": "{{ .SenderName }} σας έστειλε νέο μήνυμα", + "other": "{{ .SenderName }} και {{.Count}} άλλοι σας έστειλαν νέα μηνύματα" + } + }, + { + "id": "api.email_batching.send_batched_email_notification.time", + "translation": "{{.Hour}}:{{.Minute}} {{.TimeZone}}" + }, + { + "id": "api.email_batching.send_batched_email_notification.subject", + "translation": { + "one": "[{{.SiteName}}] Νέα ειδοποίηση για {{.Month}} {{.Day}}, {{.Year}}", + "other": "[{{.SiteName}}] Νέες ειδοποιήσεις για {{.Month}} {{.Day}}, {{.Year}}" + } + }, + { + "id": "api.email_batching.send_batched_email_notification.subTitle", + "translation": "Δείτε παρακάτω μια περίληψη των νέων μηνυμάτων σας." + }, + { + "id": "api.email_batching.send_batched_email_notification.messageButton", + "translation": "Δείτε αυτό το μήνυμα" + }, + { + "id": "api.email_batching.send_batched_email_notification.button", + "translation": "Ανοίξτε το Mattermost" + }, + { + "id": "api.email_batching.add_notification_email_to_batch.disabled.app_error", + "translation": "Η ομαδοποίηση μηνυμάτων ηλεκτρονικού ταχυδρομείου έχει απενεργοποιηθεί από το διαχειριστή του συστήματος." + }, + { + "id": "api.file.upload_file.large_image.app_error", + "translation": "Δεν ήταν δυνατή η μεταφόρτωση του αρχείου που υπερβαίνει τις μέγιστες διαστάσεις: {{.Filename}}" + }, + { + "id": "api.file.upload_file.incorrect_number_of_client_ids.app_error", + "translation": "Δεν είναι δυνατή η μεταφόρτωση αρχείων. Έχετε {{.NumClientIds}} client_ids για {{.NumFiles}} αρχεία." + }, + { + "id": "api.file.upload_file.incorrect_channelId.app_error", + "translation": "Δεν είναι δυνατή η μεταφόρτωση του αρχείου. Εσφαλμένο αναγνωριστικό καναλιού: {{.channelId}}" + }, + { + "id": "api.file.test_connection_s3_bucket_does_not_exist.app_error", + "translation": "Βεβαιωθείτε ότι ο Amazon S3 bucket είναι διαθέσιμος και επαληθεύστε τα δικαιώματα του." + }, + { + "id": "api.file.test_connection_s3_auth.app_error", + "translation": "Δεν είναι δυνατή η σύνδεση στο S3. Επαληθεύστε τις παραμέτρους εξουσιοδότησης σύνδεσης και τις ρυθμίσεις ελέγχου ταυτότητας για το Amazon S3." + }, + { + "id": "api.file.read_file.reading_local.app_error", + "translation": "Παρουσιάστηκε σφάλμα ανάγνωσης από τον τοπικό χώρο αποθήκευσης αρχείων του διακομιστή." + }, + { + "id": "api.file.no_driver.app_error", + "translation": "Δεν έχει επιλεγεί πρόγραμμα οδήγησης αρχείου." + }, + { + "id": "api.file.get_public_link.no_post.app_error", + "translation": "Δεν είναι δυνατή η λήψη δημόσιου συνδέσμου για το αρχείο. Το αρχείο πρέπει να επισυνάπτεται σε μια δημοσίευση που μπορεί να διαβαστεί από τον τρέχοντα χρήστη." + }, + { + "id": "api.file.get_public_link.disabled.app_error", + "translation": "Οι δημόσιοι σύνδεσμοι έχουν απενεργοποιηθεί." + }, + { + "id": "api.file.get_file_thumbnail.no_thumbnail.app_error", + "translation": "Το αρχείο δεν έχει εικόνα μικρογραφίας." + }, + { + "id": "api.file.get_file_preview.no_preview.app_error", + "translation": "Το αρχείο δεν έχει εικόνα προεπισκόπησης." + }, + { + "id": "api.file.get_file.public_invalid.app_error", + "translation": "Ο δημόσιος σύνδεσμος δε φαίνεται να είναι έγκυρος." + }, + { + "id": "api.file.attachments.disabled.app_error", + "translation": "Η σύναψη αρχείων έχει απενεργοποιηθεί σε αυτόν τον διακομιστή." + }, + { + "id": "api.error_set_first_admin_visit_marketplace_status", + "translation": "Σφάλμα κατά την προσπάθεια αποθήκευσης της κατάστασης της πρώτης επίσκεψης του διαχειριστή στο marketplace." + }, + { + "id": "api.oauth.get_access_token.bad_client_secret.app_error", + "translation": "invalid_request: Λείπει το client_secret." + }, + { + "id": "api.oauth.get_access_token.bad_client_id.app_error", + "translation": "invalid_request: Λάθος client_id." + }, + { + "id": "api.oauth.click_redirect", + "translation": "Εάν δεν ανακατευθυνθείτε αυτόματα, κάντε κλικ στον σύνδεσμο" + }, + { + "id": "api.oauth.authorize_oauth.disabled.app_error", + "translation": "Ο διαχειριστής του συστήματος έχει απενεργοποιήσει τον πάροχο υπηρεσιών OAuth2." + }, + { + "id": "api.oauth.allow_oauth.turn_off.app_error", + "translation": "Ο διαχειριστής του συστήματος έχει απενεργοποιήσει τον πάροχο υπηρεσιών OAuth2." + }, + { + "id": "api.migrate_to_saml.error", + "translation": "Δεν είναι δυνατή η μετεγκατάσταση SAML." + }, + { + "id": "api.marshal_error", + "translation": "σφάλμα διευθέτησης" + }, + { + "id": "api.license.request_trial_license.fail_get_user_count.app_error", + "translation": "Δεν είναι δυνατή η λήψη μιας δοκιμαστικής άδειας, δοκιμάστε ξανά ή επικοινωνήστε με το support@mattermost.com. Δεν είναι δυνατή η απόκτηση του αριθμού των εγγεγραμμένων χρηστών." + }, + { + "id": "api.license.request_trial_license.app_error", + "translation": "Δεν είναι δυνατή η λήψη μιας δοκιμαστικής άδειας, δοκιμάστε ξανά ή επικοινωνήστε με το support@mattermost.com." + }, + { + "id": "api.license.request-trial.bad-request.terms-not-accepted", + "translation": "Πρέπει να αποδεχτείτε τη Συμφωνία Αξιολόγησης Λογισμικού της Mattermost και την Πολιτική Απορρήτου για να ζητήσετε άδεια χρήσης." + }, + { + "id": "api.license.request-trial.bad-request", + "translation": "Ο αριθμός των χρηστών που ζητήθηκαν δεν είναι σωστός." + }, + { + "id": "api.license.remove_expired_license.failed.error", + "translation": "Απέτυχε να αποσταλεί επιτυχώς το μήνυμα ηλεκτρονικού ταχυδρομείου για την απενεργοποίηση της άδειας χρήσης." + }, + { + "id": "api.license.client.old_format.app_error", + "translation": "Η νέα μορφή για την άδεια χρήσης πελάτη δεν υποστηρίζεται ακόμη. Παρακαλούμε καθορίστε format=old στη συμβολοσειρά ερωτήματος." + }, + { + "id": "api.license.add_license.unique_users.app_error", + "translation": "Αυτή η άδεια χρήσης υποστηρίζει μόνο {{.Users}} χρήστες, όταν το σύστημά σας έχει {{.Count}} μοναδικούς χρήστες. Οι μοναδικοί χρήστες υπολογίζονται ξεχωριστά ανά διεύθυνση ηλεκτρονικού ταχυδρομείου. Μπορείτε να δείτε τον συνολικό αριθμό χρηστών στο Αναφορές Ιστότοπου -> Προβολή Στατιστικών." + }, + { + "id": "api.license.add_license.save_active.app_error", + "translation": "Το αναγνωριστικό ενεργής άδειας χρήσης δεν αποθηκεύτηκε σωστά." + }, + { + "id": "api.license.add_license.save.app_error", + "translation": "Η άδεια χρήσης δεν αποθηκεύτηκε σωστά." + }, + { + "id": "api.license.add_license.invalid_count.app_error", + "translation": "Δεν είναι δυνατή η καταμέτρηση του συνόλου των μοναδικών χρηστών." + }, + { + "id": "api.license.add_license.invalid.app_error", + "translation": "Μη έγκυρο αρχείο άδειας χρήσης." + }, + { + "id": "api.license.add_license.expired.app_error", + "translation": "Η άδεια χρήσης είτε έχει λήξει είτε δεν έχει ξεκινήσει ακόμη." + }, + { + "id": "api.ldap_groups.license_error", + "translation": "Η άδειά σας δεν υποστηρίζει ομάδες ldap" + }, + { + "id": "api.ldap_groups.existing_user_name_error", + "translation": "το όνομα του γκρουπ υπάρχει ήδη ως όνομα χρήστη" + }, + { + "id": "api.ldap_groups.existing_reserved_name_error", + "translation": "το όνομα του γκρουπ υπάρχει ήδη ως δεσμευμένο όνομα" + }, + { + "id": "api.ldap_groups.existing_group_name_error", + "translation": "Το όνομα του γκρουπ υπάρχει ήδη" + }, + { + "id": "api.ldap_group.not_found", + "translation": "Δε βρέθηκε ομάδα ldap" + }, + { + "id": "api.job.unable_to_download_job", + "translation": "Δεν είναι δυνατή η λήψη αυτής της εργασίας" + }, + { + "id": "api.job.unable_to_create_job.incorrect_job_type", + "translation": "Ο τύπος εργασίας της εργασίας που προσπαθείτε να δημιουργήσετε δεν είναι έγκυρος" + }, + { + "id": "api.job.retrieve.nopermissions", + "translation": "Οι τύποι εργασίας μιας εργασίας που προσπαθείτε να ανακτήσετε δεν περιέχουν δικαιώματα" + }, + { + "id": "api.io_error", + "translation": "σφάλμα εισαγωγής/εξαγωγής" + }, + { + "id": "api.invalid_channel", + "translation": "Το κανάλι που αναφέρεται στο αίτημα δεν ανήκει στο χρήστη" + }, + { + "id": "api.incoming_webhook.invalid_username.app_error", + "translation": "Μη έγκυρο όνομα χρήστη." + }, + { + "id": "api.image.get.app_error", + "translation": "Δεν είναι δυνατή η ανάλυση της ζητούμενης URL εικόνας." + }, + { + "id": "api.file.upload_file.too_large_detailed.app_error", + "translation": "Δεν είναι δυνατή η μεταφόρτωση του αρχείου {{.Filename}}. Τα {{.Length}} bytes υπερβαίνουν το μέγιστο επιτρεπόμενο {{.Limit}} bytes." + }, + { + "id": "api.file.upload_file.read_request.app_error", + "translation": "Δεν είναι δυνατή η μεταφόρτωση αρχείων. Σφάλμα ανάγνωσης ή ανάλυσης δεδομένων αιτήματος." + }, + { + "id": "api.file.upload_file.read_form_value.app_error", + "translation": "Δεν είναι δυνατή η μεταφόρτωση αρχείων. Σφάλμα κατά την ανάγνωση της τιμής για {{.Formname}}." + }, + { + "id": "api.file.upload_file.multiple_channel_ids.app_error", + "translation": "Δεν είναι δυνατή η μεταφόρτωση αρχείων. Πολλαπλά αντικρουόμενα channel_ids." + }, + { + "id": "api.file.upload_file.large_image_detailed.app_error", + "translation": "Οι διαστάσεις του {{.Filename}} ({{.Width}} επί {{.Height}} pixels) υπερβαίνουν τα όρια." + }, + { + "id": "api.post.create_post.root_id.app_error", + "translation": "Μη έγκυρη παράμετρος RootId." + }, + { + "id": "api.post.create_post.channel_root_id.app_error", + "translation": "Μη έγκυρο ChannelId για την παράμετρο RootId." + }, + { + "id": "api.post.create_post.can_not_post_to_deleted.error", + "translation": "Δεν μπορείτε να δημοσιεύσετε σε διαγραμμένο κανάλι." + }, + { + "id": "api.post.check_for_out_of_channel_mentions.message.one", + "translation": "Ο/Η @{{.Username}} δεν ειδοποιήθηκε από αυτή την αναφορά επειδή δεν είναι στο κανάλι." + }, + { + "id": "api.post.check_for_out_of_channel_mentions.message.multiple", + "translation": "Οι @{{.Usernames}} και @{{.LastUsername}} δεν ειδοποιήθηκαν από αυτή την αναφορά επειδή δεν είναι στο κανάλι." + }, + { + "id": "api.post.check_for_out_of_channel_groups_mentions.message.multiple", + "translation": "Οι @{{.Usernames}} και @{{.LastUsername}} δεν ειδοποιήθηκαν από αυτή την αναφορά επειδή δεν είναι στο κανάλι. Δεν μπορούν να προστεθούν στο κανάλι επειδή δεν είναι μέλη των συνδεδεμένων γκρουπ. Για να τους προσθέσετε σε αυτό το κανάλι, πρέπει να προστεθούν στα συνδεδεμένα γκρουπ." + }, + { + "id": "api.post.check_for_out_of_channel_groups_mentions.message.one", + "translation": "Ο/Η @{{.Username}} δεν ειδοποιήθηκε από αυτή την αναφορά επειδή δεν είναι στο κανάλι. Δεν μπορεί να προστεθεί στο κανάλι επειδή δεν είναι μέλος των συνδεδεμένων γκρουπ. Για να τον/την προσθέσετε σε αυτό το κανάλι, πρέπει να προστεθεί στα συνδεδεμένα γκρουπ." + }, + { + "id": "api.post.check_for_out_of_channel_group_users.message.none", + "translation": "Το @{{.GroupName}} δεν έχει μέλη σε αυτή την ομάδα" + }, + { + "id": "api.plugin.verify_plugin.app_error", + "translation": "Δεν είναι δυνατή η επαλήθευση της υπογραφής του plugin." + }, + { + "id": "api.plugin.upload.no_file.app_error", + "translation": "Λείπει αρχείο στο αίτημα πολλαπλών μερών / φόρμας." + }, + { + "id": "api.plugin.upload.file.app_error", + "translation": "Δεν είναι δυνατό το άνοιγμα αρχείου σε αίτημα πολλαπλών τμημάτων / φόρμας." + }, + { + "id": "api.plugin.upload.array.app_error", + "translation": "Ο πίνακας αρχείων είναι κενός σε αίτημα πολλαπλών τμημάτων / φορμών." + }, + { + "id": "api.plugin.install.download_failed.app_error", + "translation": "Προέκυψε σφάλμα κατά τη λήψη του plugin." + }, + { + "id": "api.oauth.singup_with_oauth.invalid_link.app_error", + "translation": "Ο σύνδεσμος εγγραφής δε φαίνεται να είναι έγκυρος." + }, + { + "id": "api.oauth.singup_with_oauth.expired_link.app_error", + "translation": "Ο σύνδεσμος εγγραφής έχει λήξει." + }, + { + "id": "api.oauth.singup_with_oauth.disabled.app_error", + "translation": "Η εγγραφή χρήστη είναι απενεργοποιημένη." + }, + { + "id": "api.oauth.revoke_access_token.get.app_error", + "translation": "Σφάλμα κατά τη λήψη διακριτικού πρόσβασης από τη ΒΔ πριν από τη διαγραφή." + }, + { + "id": "api.oauth.revoke_access_token.del_token.app_error", + "translation": "Σφάλμα διαγραφής του διακριτικού πρόσβασης από τη ΒΔ." + }, + { + "id": "api.oauth.revoke_access_token.del_session.app_error", + "translation": "Σφάλμα διαγραφής περιόδου σύνδεσης από τη ΒΔ." + }, + { + "id": "api.oauth.register_oauth_app.turn_off.app_error", + "translation": "Ο διαχειριστής του συστήματος έχει απενεργοποιήσει τον πάροχο υπηρεσιών OAuth2." + }, + { + "id": "api.oauth.invalid_state_token.app_error", + "translation": "Μη έγκυρο διακριτικό κατάστασης." + }, + { + "id": "api.oauth.get_access_token.refresh_token.app_error", + "translation": "invalid_grant: Μη έγκυρο κουπόνι ανανέωσης." + }, + { + "id": "api.oauth.get_access_token.redirect_uri.app_error", + "translation": "invalid_request: Το παρεχόμενο redirect_uri δεν αντιστοιχεί στον κωδικό εξουσιοδότησης redirect_uri." + }, + { + "id": "api.oauth.get_access_token.missing_refresh_token.app_error", + "translation": "invalid_request: Λείπει το refresh_token." + }, + { + "id": "api.oauth.get_access_token.missing_code.app_error", + "translation": "invalid_request: Λείπει ο κωδικός." + }, + { + "id": "api.oauth.get_access_token.internal_user.app_error", + "translation": "server_error: Παρουσιάστηκε σφάλμα εσωτερικού διακομιστή κατά τη λήψη χρήστη από τη βάση δεδομένων." + }, + { + "id": "api.oauth.get_access_token.internal_session.app_error", + "translation": "server_error: Παρουσιάστηκε σφάλμα εσωτερικού διακομιστή κατά την αποθήκευση περιόδου λειτουργίας στη βάση δεδομένων." + }, + { + "id": "api.oauth.get_access_token.internal_saving.app_error", + "translation": "server_error: Παρουσιάστηκε σφάλμα εσωτερικού διακομιστή κατά την αποθήκευση του διακριτικού πρόσβασης στη βάση δεδομένων." + }, + { + "id": "api.oauth.get_access_token.internal.app_error", + "translation": "server_error: Παρουσιάστηκε σφάλμα εσωτερικού διακομιστή κατά την πρόσβαση στη βάση δεδομένων." + }, + { + "id": "api.oauth.get_access_token.bad_grant.app_error", + "translation": "invalid_request: Λάθος grant_type." + }, + { + "id": "api.oauth.allow_oauth.redirect_callback.app_error", + "translation": "invalid_request: Το παρεχόμενο redirect_uri δεν ταιριάζει με το καταχωρημένο callback_url." + }, + { + "id": "api.oauth.get_access_token.credentials.app_error", + "translation": "invalid_client: Μη έγκυρα διαπιστευτήρια πελάτη." + }, + { + "id": "api.oauth.get_access_token.expired_code.app_error", + "translation": "invalid_grant: Μη έγκυρος ή ληγμένος κωδικός εξουσιοδότησης." + }, + { + "id": "api.oauth.get_access_token.disabled.app_error", + "translation": "Ο διαχειριστής του συστήματος έχει απενεργοποιήσει τον πάροχο υπηρεσιών OAuth2." + }, + { + "id": "api.templates.reset_subject", + "translation": "[{{ .SiteName }}] Επαναφέρετε τον κωδικό πρόσβασης σας" + }, + { + "id": "api.templates.cloud_welcome_email.add_apps_sub_info", + "translation": "Βελτιώστε την εργασία σας με εργαλεία όπως το Github, το Ημερολόγιο της Google και το Chrome. Εξερευνήστε όλες τις ενσωματώσεις που διαθέτουμε" + }, + { + "id": "api.license.request-trial.can-start-trial.error", + "translation": "Δεν ήταν δυνατός ο έλεγχος εάν μπορεί να ξεκινήσει μια δοκιμαστική άδεια" + }, + { + "id": "api.team.team_icon.update.app_error", + "translation": "Παρουσιάστηκε σφάλμα κατά την ενημέρωση του εικονιδίου της ομάδας." + }, + { + "id": "api.team.set_team_icon.write_file.app_error", + "translation": "Δεν ήταν δυνατή η αποθήκευση του εικονιδίου ομάδας." + }, + { + "id": "api.team.set_team_icon.get_team.app_error", + "translation": "Παρουσιάστηκε σφάλμα κατά τη λήψη της ομάδας." + }, + { + "id": "api.team.set_team_icon.encode.app_error", + "translation": "Δεν ήταν δυνατή η κωδικοποίηση του εικονιδίου ομάδας." + }, + { + "id": "api.team.set_team_icon.decode.app_error", + "translation": "Δεν ήταν δυνατή η αποκωδικοποίηση του εικονιδίου ομάδας." + }, + { + "id": "api.team.set_team_icon.check_image_limits.app_error", + "translation": "Ο έλεγχος ορίων εικόνας απέτυχε. Η ανάλυση είναι πολύ υψηλή." + }, + { + "id": "api.team.search_teams.pagination_not_implemented.public_team_search", + "translation": "Η σελιδοποίηση δεν εφαρμόζεται για την αναζήτηση μόνο δημοσίων ομάδων." + }, + { + "id": "api.team.search_teams.pagination_not_implemented.private_team_search", + "translation": "Η σελιδοποίηση δεν εφαρμόζεται για την αναζήτηση μόνο ιδιωτικών ομάδων." + }, + { + "id": "api.team.remove_user_from_team.missing.app_error", + "translation": "Ο χρήστης δε φαίνεται να είναι μέλος αυτής της ομάδας." + }, + { + "id": "api.team.remove_team_icon.get_team.app_error", + "translation": "Παρουσιάστηκε σφάλμα κατά τη λήψη της ομάδας." + }, + { + "id": "api.team.remove_member.group_constrained.app_error", + "translation": "Δεν είναι δυνατή η κατάργηση ενός χρήστη από μια ομάδα περιορισμένου-γκρουπ." + }, + { + "id": "api.team.move_channel.success", + "translation": "Αυτό το κανάλι έχει μεταφερθεί σε αυτή την ομάδα από την %v." + }, + { + "id": "api.team.leave.left", + "translation": "%v έφυγε από την ομάδα." + }, + { + "id": "api.team.join_user_to_team.allowed_domains.app_error", + "translation": "Ο χρήστης δεν μπορεί να προστεθεί καθώς δεν επιτρέπεται ο τομέας στον οποίο ανήκει ο λογαριασμός του. Επικοινωνήστε με το Διαχειριστή συστήματος για περισσότερες λεπτομέρειες." + }, + { + "id": "api.team.join_team.post_and_forget", + "translation": "%v εισήλθαν στην ομάδα." + }, + { + "id": "api.team.is_team_creation_allowed.domain.app_error", + "translation": "Ο χρήστης δεν μπορεί να προστεθεί καθώς δεν επιτρέπεται ο τομέας στον οποίο ανήκει ο λογαριασμός του. Επικοινωνήστε με το Διαχειριστή συστήματος για περισσότερες λεπτομέρειες." + }, + { + "id": "api.team.is_team_creation_allowed.disabled.app_error", + "translation": "Η δημιουργία ομάδας έχει απενεργοποιηθεί. Ρωτήστε τον Διαχειριστή συστήματος για περισσότερες λεπτομέρειες." + }, + { + "id": "api.team.invite_members.no_one.app_error", + "translation": "Δεν υπάρχει κανείς να προσκαλεστεί." + }, + { + "id": "api.team.invite_members.invalid_email.app_error", + "translation": "Οι ακόλουθες διευθύνσεις ηλεκτρονικού ταχυδρομείου δεν ανήκουν σε αποδεκτό τομέα: {{.Addresses}}. Για περισσότερες λεπτομέρειες, επικοινωνήστε με το Διαχειριστή συστήματος." + }, + { + "id": "api.team.invite_members.disabled.app_error", + "translation": "Οι προσκλήσεις μέσω email είναι απενεργοποιημένες." + }, + { + "id": "api.team.invite_guests_to_channels.invalid_body.app_error", + "translation": "Μη έγκυρο ή ελλιπές κυρίως σώμα της αίτησης." + }, + { + "id": "api.team.invite_guests.channel_in_invalid_team.app_error", + "translation": "Τα κανάλια της πρόσκλησης πρέπει να είναι μέρος της ομάδας της πρόσκλησης." + }, + { + "id": "api.post.update_post.system_message.app_error", + "translation": "Δεν είναι δυνατή η ενημέρωση του μηνύματος του συστήματος." + }, + { + "id": "api.post.update_post.permissions_time_limit.app_error", + "translation": "Η επεξεργασία ανάρτησης επιτρέπεται μόνο για {{.timeLimit}} δευτερόλεπτα. Παρακαλούμε ρωτήστε τον Διαχειριστή συστήματος για περισσότερες λεπτομέρειες." + }, + { + "id": "api.post.update_post.permissions_details.app_error", + "translation": "Διαγράφηκε ήδη η ανάρτηση με αναγνωριστικό={{. PostId}}." + }, + { + "id": "api.post.update_post.can_not_update_post_in_deleted.error", + "translation": "Δεν είναι δυνατή η ενημέρωση μιας ανάρτησης σε ένα διαγραμμένο κανάλι." + }, + { + "id": "api.post.send_notifications_and_forget.push_message", + "translation": "σας έστειλε ένα μήνυμα." + }, + { + "id": "api.post.send_notifications_and_forget.push_image_only", + "translation": " επισύναψε ένα αρχείο." + }, + { + "id": "api.post.send_notifications_and_forget.push_general_message", + "translation": " δημοσίευσε ένα μήνυμα." + }, + { + "id": "api.post.send_notifications_and_forget.push_explicit_mention", + "translation": " σας ανέφερε." + }, + { + "id": "api.post.send_notification_and_forget.push_comment_on_thread", + "translation": " σχολίασε ένα νήμα στο οποίο συμμετείχατε." + }, + { + "id": "api.post.send_notification_and_forget.push_comment_on_post", + "translation": " σχολίασε την ανάρτησή σας." + }, + { + "id": "api.post.send_notification_and_forget.push_channel_mention", + "translation": " ειδοποίησε το κανάλι." + }, + { + "id": "api.post.patch_post.can_not_update_post_in_deleted.error", + "translation": "Δεν είναι δυνατή η ενημέρωση μιας ανάρτησης σε ένα διαγραμμένο κανάλι." + }, + { + "id": "api.post.link_preview_disabled.app_error", + "translation": "Οι προεπισκοπήσεις συνδέσμων έχουν απενεργοποιηθεί από τον διαχειριστή συστήματος." + }, + { + "id": "api.post.get_message_for_notification.images_sent", + "translation": { + "one": "{{.Count}} εικόνα εστάλη: {{.Filenames}}", + "other": "{{.Count}} εικόνες εστάλησαν: {{.Filenames}}" + } + }, + { + "id": "api.post.get_message_for_notification.files_sent", + "translation": { + "one": "{{.Count}} αρχείο εστάλη: {{.Filenames}}", + "other": "{{.Count}} αρχεία εστάλησαν: {{.Filenames}}" + } + }, + { + "id": "api.post.error_get_post_id.pending", + "translation": "Δεν είναι δυνατή η λήψη της εκκρεμής ανάρτησης." + }, + { + "id": "api.post.do_action.action_integration.app_error", + "translation": "Σφάλμα ενοποιητικής ενέργειας." + }, + { + "id": "api.post.do_action.action_id.app_error", + "translation": "Μη έγκυρο αναγνωριστικό ενέργειας." + }, + { + "id": "api.post.disabled_here", + "translation": "Το @here έχει απενεργοποιηθεί επειδή το κανάλι έχει περισσότερους από {{.Users}} χρήστες." + }, + { + "id": "api.post.disabled_channel", + "translation": "Το @channel έχει απενεργοποιηθεί επειδή το κανάλι έχει περισσότερους από {{.Users}} χρήστες." + }, + { + "id": "api.post.disabled_all", + "translation": "Το @all έχει απενεργοποιηθεί επειδή το κανάλι έχει περισσότερους από {{.Users}} χρήστες." + }, + { + "id": "api.post.delete_post.can_not_delete_post_in_deleted.error", + "translation": "Δεν είναι δυνατή η διαγραφή μιας ανάρτησης σε ένα διαγραμμένο κανάλι." + }, + { + "id": "api.post.deduplicate_create_post.failed_to_get", + "translation": "Αποτυχία ανάκτησης αρχικής ανάρτησης μετά την κατάργηση διπλότυπων δεδομένων ενός προγράμματος-πελάτη που επαναλαμβάνει το ίδιο αίτημα." + }, + { + "id": "api.post.deduplicate_create_post.pending", + "translation": "Απορρίφθηκε η ανάρτηση επειδή άλλο πρόγραμμα-πελάτης κάνει το ίδιο αίτημα." + }, + { + "id": "api.post.create_webhook_post.creating.app_error", + "translation": "Σφάλμα κατά τη δημιουργία ανάρτησης." + }, + { + "id": "api.templates.reset_body.title", + "translation": "Επαναφορά του κωδικού πρόσβασης σας" + }, + { + "id": "api.templates.reset_body.subTitle", + "translation": "Κάντε κλικ στο κουμπί παρακάτω για να επαναφέρετε τον κωδικό πρόσβασης σας. Εάν δεν το ζητήσατε εσείς, μπορείτε να αγνοήσετε με ασφάλεια αυτό το email." + }, + { + "id": "api.templates.reset_body.info", + "translation": "Ο σύνδεσμος επαναφοράς κωδικού πρόσβασης λήγει σε 24 ώρες." + }, + { + "id": "api.templates.reset_body.button", + "translation": "Επαναφορά κωδικού πρόσβασης" + }, + { + "id": "api.templates.remove_expired_license.subject", + "translation": "Η άδεια χρήσης Mattermost Enterprise έχει απενεργοποιηθεί." + }, + { + "id": "api.templates.remove_expired_license.body.title", + "translation": "Η άδεια χρήσης σας Enterprise Edition έχει λήξει και ορισμένες δυνατότητες ενδέχεται να είναι απενεργοποιημένες. Παρακαλούμε ανανεώστε την άδειά σας τώρα." + }, + { + "id": "api.templates.remove_expired_license.body.renew_button", + "translation": "Ανανέωση άδειας χρήσης τώρα" + }, + { + "id": "api.templates.post_body.button", + "translation": "Προβολή μηνύματος" + }, + { + "id": "api.templates.password_change_subject", + "translation": "[{{ .SiteName }}] Ο κωδικός πρόσβασης σας έχει ενημερωθεί" + }, + { + "id": "api.templates.password_change_body.title", + "translation": "Ο κωδικός πρόσβασης σας έχει ενημερωθεί" + }, + { + "id": "api.templates.password_change_body.info", + "translation": "Ο κωδικός πρόσβασης σας της {{.TeamDisplayName}} στο {{ .TeamURL }} ενημερώθηκε από την {{.Method}}." + }, + { + "id": "api.templates.mfa_deactivated_body.title", + "translation": "Ο έλεγχος ταυτότητας πολλαπλών παραγόντων (MFA) καταργήθηκε" + }, + { + "id": "api.templates.mfa_deactivated_body.info", + "translation": "Ο έλεγχος ταυτότητας πολλαπλών παραγόντων (MFA) καταργήθηκε από τον λογαριασμό σας στο {{ .SiteURL }}." + }, + { + "id": "api.templates.mfa_change_subject", + "translation": "[{{ .SiteName }}] Η ταυτότητα πολλών παραγόντων (MFA) σας ενημερώθηκε" + }, + { + "id": "api.templates.mfa_activated_body.title", + "translation": "Προστέθηκε έλεγχος ταυτότητας πολλών παραγόντων (MFA)" + }, + { + "id": "api.templates.mfa_activated_body.info", + "translation": "Ο έλεγχος ταυτότητας πολλών παραγόντων (MFA) έχει προστεθεί στον λογαριασμό σας στο {{.SiteURL}}." + }, + { + "id": "api.templates.invite_subject", + "translation": "[{{.SiteName}}] Ο χρήστης {{.SenderName}} σας προσκάλεσε να συμμετάσχετε στην ομάδα {{.TeamDisplayName}}" + }, + { + "id": "api.templates.invite_guest_subject", + "translation": "[{{.SiteName}}] Ο χρήστης {{.SenderName}} σας προσκάλεσε να συμμετάσχετε στην ομάδα {{.TeamDisplayName}} ως επισκέπτης" + }, + { + "id": "api.templates.invite_body_guest.subTitle", + "translation": "Έχετε προσκληθεί ως επισκέπτης για να συνεργαστείτε με την ομάδα" + }, + { + "id": "api.templates.invite_body_footer.title", + "translation": "Τι είναι το Mattermost;" + }, + { + "id": "api.templates.invite_body_footer.learn_more", + "translation": "Μάθετε περισσότερα" + }, + { + "id": "api.templates.invite_body_footer.info", + "translation": "Το Mattermost είναι μια ευέλικτη πλατφόρμα ανταλλαγής μηνυμάτων ανοιχτού κώδικα που επιτρέπει την ασφαλή ομαδική συνεργασία." + }, + { + "id": "api.templates.invite_body.title", + "translation": "Ο χρήστης {{.SenderName}} σας προσκάλεσε να συμμετάσχετε στην ομάδα {{.TeamDisplayName}}." + }, + { + "id": "api.templates.invite_body.subTitle", + "translation": "Ξεκινήστε να συνεργάζεστε με την ομάδα σας στο Mattermost" + }, + { + "id": "api.templates.invite_body.button", + "translation": "Εγγραφείτε τώρα" + }, + { + "id": "api.templates.email_warning", + "translation": "Εάν δεν πραγματοποιήσατε εσείς αυτή την αλλαγή, επικοινωνήστε με το διαχειριστή του συστήματος." + }, + { + "id": "api.templates.email_organization", + "translation": "Στάλθηκε από " + }, + { + "id": "api.templates.email_info3", + "translation": "Η ομάδα {{.SiteName}}" + }, + { + "id": "api.templates.email_info2", + "translation": "Τις καλύτερες ευχές," + }, + { + "id": "api.templates.email_info1", + "translation": "Για οποιαδήποτε απορία, στείλτε μας email ανά πάσα στιγμή: " + }, + { + "id": "api.templates.email_footer", + "translation": "Για να αλλάξετε τις προτιμήσεις ειδοποιήσεων, συνδεθείτε στον ιστότοπο της ομάδας σας και μεταβείτε στις Ρυθμίσεις λογαριασμού > Ειδοποιήσεις." + }, + { + "id": "api.templates.email_change_verify_subject", + "translation": "[{{ .SiteName }}] Επαληθεύστε τη νέα διεύθυνση email" + }, + { + "id": "api.templates.email_change_verify_body.title", + "translation": "Ενημερώσατε το email σας" + }, + { + "id": "api.templates.email_change_verify_body.info", + "translation": "Για να ολοκληρώσετε την ενημέρωση της διεύθυνσης email σας για την {{.TeamDisplayName}}, κάντε κλικ στον παρακάτω σύνδεσμο για να επιβεβαιώσετε ότι αυτή είναι η σωστή διεύθυνση." + }, + { + "id": "api.templates.email_change_verify_body.button", + "translation": "Επαλήθευση Email" + }, + { + "id": "api.templates.email_change_subject", + "translation": "[{{.SiteName}}] Η διεύθυνση email σας άλλαξε" + }, + { + "id": "api.templates.email_change_body.title", + "translation": "Ενημερώσατε το email σας" + }, + { + "id": "api.templates.email_change_body.info", + "translation": "Η διεύθυνση email σας για την {{.TeamDisplayName}} άλλαξε σε {{.NewEmail}}." + }, + { + "id": "api.templates.deactivate_subject", + "translation": "[{{ .SiteName }}] Ο λογαριασμός σας στο {{ .ServerURL }} έχει απενεργοποιηθεί" + }, + { + "id": "api.templates.deactivate_body.warning", + "translation": "Εάν αυτή η αλλαγή δεν έγινε με δική σας πρωτοβουλία ή εάν θέλετε να ενεργοποιήσετε εκ νέου τον λογαριασμό σας, επικοινωνήστε με τον διαχειριστή του συστήματός σας." + }, + { + "id": "api.templates.deactivate_body.title", + "translation": "Ο λογαριασμός σας έχει απενεργοποιηθεί στο {{ .ServerURL }}" + }, + { + "id": "api.templates.deactivate_body.info", + "translation": "Απενεργοποιήσατε τον λογαριασμό σας στο {{.SiteURL}}." + }, + { + "id": "api.templates.cloud_welcome_email.title", + "translation": "Η δοκιμαστική έκδοση 14 ημερών του χώρου εργασίας {{.WorkSpace}} είναι έτοιμη!" + }, + { + "id": "api.templates.cloud_welcome_email.subtitle_info", + "translation": "Ακολουθήστε τα παρακάτω βήματα για να δημιουργήσετε τις ομάδες σας και να αξιοποιήσετε στο έπακρο το χώρο εργασίας σας." + }, + { + "id": "api.templates.cloud_welcome_email.subtitle", + "translation": "Ρυθμίστε το χώρο εργασίας σας" + }, + { + "id": "api.templates.cloud_welcome_email.subject", + "translation": "Συγχαρητήρια!" + }, + { + "id": "api.templates.cloud_welcome_email.start_questions", + "translation": "Έχετε ερωτήσεις για το πως θα ξεκινήσετε; Στείλτε μας email στο" + }, + { + "id": "api.templates.cloud_welcome_email.signin_sub_info2", + "translation": "για την καλύτερη εμπειρία σε Η/Υ, Mac, iOS και Android." + }, + { + "id": "api.templates.cloud_welcome_email.signin_sub_info", + "translation": "Συνδεθείτε στο χώρο εργασίας σας στο" + }, + { + "id": "api.templates.cloud_welcome_email.mm_apps", + "translation": "εφαρμογές για κινητά και υπολογιστές" + }, + { + "id": "api.templates.cloud_welcome_email.invite_sub_info", + "translation": "Μοιραστείτε αυτόν τον σύνδεσμο για να προσκαλέσετε τα μέλη σας να ενταχθούν στο {{.WorkSpace}}:" + }, + { + "id": "api.templates.cloud_welcome_email.invite_info", + "translation": "Προσκαλέστε άτομα στο χώρο εργασίας σας" + }, + { + "id": "api.templates.cloud_welcome_email.info2", + "translation": "Φροντίστε να αποθηκεύσετε ή να προσθέσετε σελιδοδείκτη στον σύνδεσμό σας για μελλοντική χρήση." + }, + { + "id": "api.templates.cloud_welcome_email.info", + "translation": "Ευχαριστώ για τη δημιουργία " + }, + { + "id": "api.templates.cloud_welcome_email.download_mm_info", + "translation": "Κατεβάστε την εφαρμογή Mattermost" + }, + { + "id": "api.templates.cloud_welcome_email.button", + "translation": "Ανοίξτε το Mattermost" + }, + { + "id": "api.templates.cloud_welcome_email.app_market_place", + "translation": "αγορά εφαρμογών." + }, + { + "id": "api.templates.cloud_welcome_email.add_apps_info", + "translation": "Προσθέστε εφαρμογές στον χώρο εργασίας σας" + }, + { + "id": "api.team.update_team_member_roles.guest_and_user.app_error", + "translation": "Μη έγκυρη ενημέρωση μέλους ομάδας: Ένας χρήστης πρέπει να είναι είτε επισκέπτης είτε χρήστης, όχι και τα δύο." + }, + { + "id": "api.team.update_restricted_domains.mismatch.app_error", + "translation": "Δεν επιτρέπεται ο περιορισμός της ομάδας στο {{.Domain}} από τη διαμόρφωση συστήματος. Παρακαλούμε επικοινωνήστε με τον διαχειριστή του συστήματός σας." + }, + { + "id": "api.license.request-trial.can-start-trial.not-allowed", + "translation": "Αυτή η δοκιμαστική άδεια χρήσης για το Mattermost Enterprise Edition έχει λήξει και δεν είναι πλέον έγκυρη. Εάν θέλετε να παρατείνετε τη δοκιμαστική περίοδο, παρακαλούμε [επικοινωνήστε με την ομάδα πωλήσεών μας](https://mattermost.com/contact-us/)." + }, + { + "id": "api.upgrade_to_enterprise_status.app_error", + "translation": "Το Mattermost δεν μπόρεσε να κάνει αναβάθμιση σε Enterprise Edition." + }, + { + "id": "api.upgrade_to_enterprise.system_not_supported.app_error", + "translation": "Το Mattermost δεν μπόρεσε να κάνει αναβάθμιση σε Enterprise Edition. Αυτή η δυνατότητα θα λειτουργεί μόνο σε συστήματα Linux με αρχιτεκτονική x86-64." + }, + { + "id": "api.upgrade_to_enterprise.invalid-user.app_error", + "translation": "Το Mattermost δεν μπόρεσε να κάνει αναβάθμιση σε Enterprise Edition. Ο χρήστης του συστήματος Mattermost {{.MattermostUsername}} δεν έχει πρόσβαση εγγραφής στο απαραίτητο δυαδικό αρχείο. Ένας διαχειριστής συστήματος μπορεί να ενημερώσει τα δικαιώματα του αρχείου εκτελώντας την ακόλουθη εντολή στο διακομιστή όπου είναι εγκατεστημένο το Mattermost:\n\n```\nchown {{.MattermostUsername}} \"{{.Path}}\"\n```\n\nΑφού γίνει αλλαγή στα δικαιώματα των αρχείων, δοκιμάστε να αναβαθμίσετε ξανά το Mattermost. Όταν κάνετε αναβάθμιση και επανεκκίνηση, θυμηθείτε να επαναφέρετε τα αρχικά δικαιώματα των δυαδικών αρχείων:\n\n```\nchown {{.FileUsername}} \"{{.Path}}\"\n```" + }, + { + "id": "api.upgrade_to_enterprise.invalid-user-and-permission.app_error", + "translation": "Το Mattermost δεν μπόρεσε να κάνει αναβάθμιση σε Enterprise Edition. Ο χρήστης του συστήματος Mattermost {{.MattermostUsername}} δεν έχει πρόσβαση εγγραφής στο απαραίτητο δυαδικό αρχείο. Ένας διαχειριστής συστήματος μπορεί να ενημερώσει τα δικαιώματα του αρχείου εκτελώντας την ακόλουθη εντολή στο διακομιστή όπου είναι εγκατεστημένο το Mattermost:\n\n```\nchown {{.MattermostUsername}} \"{{.Path}}\"\nchmod +w \"{{.Path}}\"\n```\n\nΑφού γίνει αλλαγή στα δικαιώματα των αρχείων, δοκιμάστε να αναβαθμίσετε ξανά το Mattermost. Όταν κάνετε αναβάθμιση και επανεκκίνηση, θυμηθείτε να επαναφέρετε τα αρχικά δικαιώματα των δυαδικών αρχείων:\n\n```\nchown {{.FileUsername}} \"{{.Path}}\"\nchmod -w \"{{.Path}}\"\n```" + }, + { + "id": "api.upgrade_to_enterprise.invalid-permission.app_error", + "translation": "Το Mattermost δεν μπόρεσε να κάνει αναβάθμιση σε Enterprise Edition. Ο χρήστης του συστήματος Mattermost {{.MattermostUsername}} δεν έχει πρόσβαση εγγραφής στο απαραίτητο δυαδικό αρχείο. Ένας διαχειριστής συστήματος μπορεί να ενημερώσει τα δικαιώματα του αρχείου εκτελώντας την ακόλουθη εντολή στο διακομιστή όπου είναι εγκατεστημένο το Mattermost:\n\n```\nchmod +w \"{{.Path}}\"\n```\n\nΑφού γίνει αλλαγή στα δικαιώματα των αρχείων, δοκιμάστε να αναβαθμίσετε ξανά το Mattermost. Όταν κάνετε αναβάθμιση και επανεκκίνηση, θυμηθείτε να επαναφέρετε τα αρχικά δικαιώματα των δυαδικών αρχείων:\n\n```\nchmod -w \"{{.Path}}\"\n```" + }, + { + "id": "api.upgrade_to_enterprise.generic_error.app_error", + "translation": "Το Mattermost δεν μπόρεσε να κάνει αναβάθμιση σε Enterprise Edition." + }, + { + "id": "api.upgrade_to_enterprise.app_error", + "translation": "Εκτελείται ήδη μια αναβάθμιση σε Mattermost Enterprise Edition." + }, + { + "id": "api.upgrade_to_enterprise.already-enterprise.app_error", + "translation": "Δεν μπορείτε να κάνετε αναβάθμιση επειδή εκτελείτε ήδη το Mattermost Enterprise Edition." + }, + { + "id": "api.upgrade_to_enterprise.already-done.app_error", + "translation": "Έχετε ήδη αναβαθμίσει σε Mattermost Enterprise Edition. Παρακαλώ κάνετε επανεκκίνηση τον διακομιστή για να ολοκληρώσετε την αναβάθμιση." + }, + { + "id": "api.templates.welcome_subject", + "translation": "[{{ .SiteName }}] Έχετε ενταχθεί στο {{ .ServerURL }}" + }, + { + "id": "api.templates.welcome_body.title", + "translation": "Καλώς ήρθατε στην ομάδα" + }, + { + "id": "api.templates.welcome_body.info", + "translation": "Αυτή η διεύθυνση email χρησιμοποιήθηκε για τη δημιουργία λογαριασμού στο Mattermost." + }, + { + "id": "api.templates.welcome_body.button", + "translation": "Επαλήθευση Email" + }, + { + "id": "api.templates.welcome_body.app_download_info", + "translation": "Για την καλύτερη δυνατή εμπειρία, κατεβάστε τις εφαρμογές για Η/Υ, Mac, iOS και Android." + }, + { + "id": "api.templates.warn_metric_ack.subject", + "translation": "Αίτημα επικοινωνίας με τη Mattermost" + }, + { + "id": "api.templates.warn_metric_ack.footer", + "translation": "Εάν έχετε επιπλέον απορίες, επικοινωνήστε με το support@mattermost.com" + }, + { + "id": "api.templates.warn_metric_ack.body.site_url_header", + "translation": "Διεύθυνση URL ιστότοπου: " + }, + { + "id": "api.templates.warn_metric_ack.body.registered_users_header", + "translation": "Σύνολο ενεργών χρηστών: " + }, + { + "id": "api.templates.warn_metric_ack.body.diagnostic_id_header", + "translation": "Διαγνωστικό αναγνωριστικό: " + }, + { + "id": "api.templates.warn_metric_ack.body.contact_name_header", + "translation": "Επικοινωνία: " + }, + { + "id": "api.templates.warn_metric_ack.body.contact_email_header", + "translation": "Email: " + }, + { + "id": "api.templates.verify_subject", + "translation": "[{{ .SiteName }}] Επαλήθευση email" + }, + { + "id": "api.templates.verify_body.title", + "translation": "Επαληθεύστε τη διεύθυνση email σας" + }, + { + "id": "api.templates.verify_body.info", + "translation": "Αυτή η διεύθυνση email χρησιμοποιήθηκε για τη δημιουργία λογαριασμού στο Mattermost." + }, + { + "id": "api.templates.verify_body.button", + "translation": "Επαλήθευση Email" + }, + { + "id": "api.templates.username_change_subject", + "translation": "[{{ .SiteName }}] Το όνομα χρήστη σας έχει αλλάξει" + }, + { + "id": "api.templates.username_change_body.title", + "translation": "Ενημερώσατε το όνομα χρήστη σας" + }, + { + "id": "api.templates.username_change_body.info", + "translation": "Το όνομα χρήστη σας για την {{.TeamDisplayName}} έχει αλλάξει σε {{.NewUsername}}." + }, + { + "id": "api.templates.user_access_token_subject", + "translation": "[{{ .SiteName }}] Προστέθηκε διακριτικό προσωπικής πρόσβασης στον λογαριασμό σας" + }, + { + "id": "api.templates.user_access_token_body.title", + "translation": "Προστέθηκε διακριτικό προσωπικής πρόσβασης στον λογαριασμό σας" + }, + { + "id": "api.templates.user_access_token_body.info", + "translation": "Ένα προσωπικό διακριτικό πρόσβασης προστέθηκε στον λογαριασμό σας στο {{.SiteURL}}. Μπορούν να χρησιμοποιηθούν για πρόσβαση στο {{.SiteName}} με τον λογαριασμό σας." + }, + { + "id": "api.templates.signin_change_email.subject", + "translation": "[{{.SiteName}}] Η μέθοδος σύνδεσής σας έχει ενημερωθεί" + }, + { + "id": "api.templates.signin_change_email.body.title", + "translation": "Ενημερώσατε τη μέθοδο σύνδεσής σας" + }, + { + "id": "api.templates.signin_change_email.body.method_email", + "translation": "email και κωδικός πρόσβασης" + }, + { + "id": "api.templates.signin_change_email.body.info", + "translation": "Ενημερώσατε τη μέθοδο σύνδεσής σας στο {{.SiteName}} σε {{.Method}}." + }, + { + "id": "api.upload.upload_data.invalid_content_type", + "translation": "Μη έγκυρος τύπος περιεχομένου για πολλαπλή μεταφόρτωση." + }, + { + "id": "api.upload.upload_data.invalid_content_length", + "translation": "Μη έγκυρο περιεχόμενο-μήκος." + }, + { + "id": "api.upload.get_upload.forbidden.app_error", + "translation": "Αποτυχία λήψης." + }, + { + "id": "api.upgrade_to_enterprise_status.signature.app_error", + "translation": "Το Mattermost δεν μπόρεσε να κάνει αναβάθμιση σε Enterprise Edition. Δεν ήταν δυνατή η επαλήθευση της ψηφιακής υπογραφής του ληφθέντος δυαδικού αρχείου." + }, + { + "id": "api.user.add_direct_channels_and_forget.failed.error", + "translation": "Αποτυχία προσθήκης προτιμήσεων άμεσου καναλιού για τον χρήστη user_id= {{.UserId}}, team_id= {{.TeamId}}, err= {{.Error}}" + }, + { + "id": "api.user.activate_mfa.email_and_ldap_only.app_error", + "translation": "Ο έλεγχος ταυτότητας πολλών παραγόντων (MFA) δεν είναι διαθέσιμος για αυτόν τον τύπο λογαριασμού." + }, + { + "id": "api.upload.upload_data.multipart_error", + "translation": "Αποτυχία επεξεργασίας των δεδομένων πολλαπλών τμημάτων." + }, + { + "id": "api.user.demote_user_to_guest.already_guest.app_error", + "translation": "Δεν είναι δυνατή η μετατροπή του χρήστη σε επισκέπτη επειδή είναι ήδη επισκέπτης." + }, + { + "id": "api.user.delete_user.not_enabled.app_error", + "translation": "Η λειτουργία μόνιμης διαγραφής χρηστών δεν είναι ενεργοποιημένη. Επικοινωνήστε με το διαχειριστή του συστήματός σας." + }, + { + "id": "api.user.delete_team.not_enabled.app_error", + "translation": "Η λειτουργία μόνιμης διαγραφής ομάδων δεν είναι ενεργοποιημένη. Επικοινωνήστε με το διαχειριστή του συστήματός σας." + }, + { + "id": "api.user.delete_channel.not_enabled.app_error", + "translation": "Η λειτουργία μόνιμης διαγραφής καναλιών δεν είναι ενεργοποιημένη. Επικοινωνήστε με το διαχειριστή του συστήματός σας." + }, + { + "id": "api.user.create_user.signup_link_invalid.app_error", + "translation": "Ο σύνδεσμος εγγραφής δε φαίνεται να είναι έγκυρος." + }, + { + "id": "api.user.create_user.signup_link_expired.app_error", + "translation": "Ο σύνδεσμος εγγραφής έχει λήξει." + }, + { + "id": "api.user.create_user.signup_email_disabled.app_error", + "translation": "Η εγγραφή χρήστη με email είναι απενεργοποιημένη." + }, + { + "id": "api.user.create_user.no_open_server", + "translation": "Αυτός ο διακομιστής δεν επιτρέπει ανοιχτές εγγραφές. Επικοινωνήστε με τον Διαχειριστή σας για να λάβετε πρόσκληση." + }, + { + "id": "api.user.create_user.invalid_invitation_type.app_error", + "translation": "Δεν είναι δυνατή η δημιουργία του χρήστη, μη έγκυρη πρόσκληση." + }, + { + "id": "api.user.create_user.guest_accounts.license.app_error", + "translation": "Η άδειά σας δεν υποστηρίζει λογαριασμούς επισκεπτών." + }, + { + "id": "api.user.create_user.guest_accounts.disabled.app_error", + "translation": "Οι λογαριασμοί επισκεπτών είναι απενεργοποιημένοι." + }, + { + "id": "api.user.create_user.disabled.app_error", + "translation": "Η δημιουργία χρηστών είναι απενεργοποιημένη." + }, + { + "id": "api.user.create_user.accepted_domain.app_error", + "translation": "Το email που δώσατε δεν ανήκει σε αποδεκτό τομέα. Επικοινωνήστε με τον διαχειριστή σας ή εγγραφείτε με διαφορετικό email." + }, + { + "id": "api.user.create_profile_image.initial.app_error", + "translation": "Δεν ήταν δυνατή η προσθήκη των αρχικών του χρήστη στην προεπιλεγμένη εικόνα προφίλ." + }, + { + "id": "api.user.create_profile_image.encode.app_error", + "translation": "Δεν ήταν δυνατή η κωδικοποίηση της προεπιλεγμένης εικόνας προφίλ." + }, + { + "id": "api.user.create_profile_image.default_font.app_error", + "translation": "Δεν ήταν δυνατή η δημιουργία προεπιλεγμένης γραμματοσειράς εικόνας προφίλ." + }, + { + "id": "api.user.create_password_token.error", + "translation": "Δεν είναι δυνατή η δημιουργία διακριτικού ανάκτησης κωδικού πρόσβασης" + }, + { + "id": "api.user.create_oauth_user.create.app_error", + "translation": "Δεν ήταν δυνατή η δημιουργία χρήστη από τον χρήστη αντικειμένου {{.Service}}." + }, + { + "id": "api.user.create_oauth_user.already_attached.app_error", + "translation": "Υπάρχει ήδη ένας λογαριασμός που σχετίζεται με αυτήν τη διεύθυνση email χρησιμοποιώντας μια μέθοδο σύνδεσης διαφορετική από την {{.Service}}. Συνδεθείτε χρησιμοποιώντας το {{.Auth}}." + }, + { + "id": "api.user.create_email_token.error", + "translation": "Αποτυχία δημιουργίας δεδομένων διακριτικών για επαλήθευση email" + }, + { + "id": "api.user.complete_switch_with_oauth.parse.app_error", + "translation": "Δεν ήταν δυνατή η ανάλυση των δεδομένων εξουσιοδότησης από τον χρήστη αντικειμένου {{.Service}}." + }, + { + "id": "api.user.complete_switch_with_oauth.blank_email.app_error", + "translation": "Κενό email." + }, + { + "id": "api.user.check_user_password.invalid.app_error", + "translation": "Η σύνδεση απέτυχε λόγω μη έγκυρου κωδικού πρόσβασης." + }, + { + "id": "api.user.check_user_mfa.bad_code.app_error", + "translation": "Μη έγκυρο διακριτικό ελέγχου ταυτότητας πολλών παραγόντων (MFA)." + }, + { + "id": "api.user.check_user_login_attempts.too_many.app_error", + "translation": "Ο λογαριασμός σας είναι κλειδωμένος λόγω πάρα πολλών αποτυχημένων προσπαθειών κωδικού πρόσβασης. Παρακαλώ επαναφέρετε τον κωδικό πρόσβασης σας." + }, + { + "id": "api.user.autocomplete_users.missing_team_id.app_error", + "translation": "Απαιτείται παράμετρος αναγνωριστικού ομάδας για αυτόματη συμπλήρωση ανά κανάλι." + }, + { + "id": "api.user.authorize_oauth_user.unsupported.app_error", + "translation": "{{.Service}} Το σύστημα ενιαίας πρόσβασης (SSO) μέσω OAuth 2.0 δεν είναι διαθέσιμο σε αυτόν τον διακομιστή." + }, + { + "id": "api.user.authorize_oauth_user.token_failed.app_error", + "translation": "Το αίτημα για διακριτικό απέτυχε." + }, + { + "id": "api.user.authorize_oauth_user.service.app_error", + "translation": "Το αίτημα για διακριτικό στο {{.Service}} απέτυχε." + }, + { + "id": "api.user.authorize_oauth_user.response.app_error", + "translation": "Λήφθηκε μη έγκυρη απάντηση από τον πάροχο υπηρεσιών OAuth." + }, + { + "id": "api.user.authorize_oauth_user.missing.app_error", + "translation": "Λείπει το διακριτικό πρόσβασης." + }, + { + "id": "api.user.authorize_oauth_user.invalid_state.app_error", + "translation": "Μη έγκυρη κατάσταση" + }, + { + "id": "api.user.authorize_oauth_user.bad_token.app_error", + "translation": "Κακός τύπος διακριτικού." + }, + { + "id": "api.user.authorize_oauth_user.bad_response.app_error", + "translation": "Κακή απάντηση από αίτημα διακριτικού." + }, + { + "id": "api.user.login.bot_login_forbidden.app_error", + "translation": "Απαγορεύεται η σύνδεση σε λογαριασμούς bot." + }, + { + "id": "api.user.login.blank_pwd.app_error", + "translation": "Το πεδίο κωδικού πρόσβασης δεν πρέπει να είναι κενό" + }, + { + "id": "api.user.ldap_to_email.not_ldap_account.app_error", + "translation": "Αυτός ο λογαριασμός χρήστη δε χρησιμοποιεί AD/LDAP." + }, + { + "id": "api.user.ldap_to_email.not_available.app_error", + "translation": "Το AD/LDAP δεν είναι διαθέσιμο σε αυτόν τον διακομιστή." + }, + { + "id": "api.user.get_user_by_email.permissions.app_error", + "translation": "Δεν είναι δυνατή η λήψη του χρήστη μέσω email." + }, + { + "id": "api.user.get_uploads_for_user.forbidden.app_error", + "translation": "Αποτυχία λήψης μεταφορτώσεων." + }, + { + "id": "api.user.email_to_oauth.not_available.app_error", + "translation": "Η μεταφορά ελέγχου ταυτότητας δεν έχει ρυθμιστεί ή δεν είναι διαθέσιμη σε αυτόν τον διακομιστή." + }, + { + "id": "api.user.email_to_ldap.not_available.app_error", + "translation": "Το AD/LDAP δεν είναι διαθέσιμο σε αυτόν τον διακομιστή." + }, + { + "id": "api.templates.license_up_for_renewal_title", + "translation": "Η συνδρομή σας στο Mattermost είναι έτοιμη για ανανέωση" + }, + { + "id": "api.templates.license_up_for_renewal_subtitle_two", + "translation": "Συνδεθείτε στο Λογαριασμό Πελάτη σας για να ανανεώσετε" + }, + { + "id": "api.templates.license_up_for_renewal_subtitle", + "translation": "{{.UserName}}, η συνδρομή σας έχει οριστεί να λήξει σε {{.Days}} ημέρες. Ελπίζουμε να απολαμβάνετε την ευέλικτη, ασφαλή ομαδική συνεργασία που δίνει το Mattermost. Ανανεώστε σύντομα για να διασφαλίσετε ότι η ομάδα σας θα συνεχίσει να απολαμβάνει αυτά τα οφέλη." + }, + { + "id": "api.templates.license_up_for_renewal_subject", + "translation": "Η άδειά σας είναι έτοιμη για ανανέωση" + }, + { + "id": "api.templates.license_up_for_renewal_renew_now", + "translation": "Ανανεώστε τώρα" + }, + { + "id": "api.server.license_up_for_renewal.error_generating_link", + "translation": "Αποτυχία δημιουργίας συνδέσμου για ανανέωση άδειας χρήσης" + }, + { + "id": "api.server.license_up_for_renewal.error_sending_email", + "translation": "Αποτυχία αποστολής άδειας χρήσης για ανανέωση μηνυμάτων emails" + } +] diff --git a/server/i18n/en.json b/server/i18n/en.json index 3aeec898291..4b07187ab7d 100644 --- a/server/i18n/en.json +++ b/server/i18n/en.json @@ -295,6 +295,14 @@ "id": "api.channel.get_channel_moderations.license.error", "translation": "Your license does not support channel moderation" }, + { + "id": "api.channel.gm_to_channel_conversion.not_allowed_for_user.request_error", + "translation": "User is not allowed to convert group message to private channel" + }, + { + "id": "api.channel.group_message.converted.to_private_channel", + "translation": "{{.ConvertedByUsername}} created this channel from a group message with {{.GMMembers}}." + }, { "id": "api.channel.guest_join_channel.post_and_forget", "translation": "%v joined the channel as guest." @@ -339,6 +347,10 @@ "id": "api.channel.patch_update_channel.forbidden.app_error", "translation": "Failed to update the channel." }, + { + "id": "api.channel.patch_update_channel.update_direct_or_group_messages_not_allowed.app_error", + "translation": "You are not allowed to update the name, display_name, and purpose of direct or group messages." + }, { "id": "api.channel.post_channel_privacy_message.error", "translation": "Failed to post channel privacy update message." @@ -431,6 +443,10 @@ "id": "api.channel.update_channel.typechange.app_error", "translation": "Channel type cannot be updated." }, + { + "id": "api.channel.update_channel.update_direct_or_group_messages_not_allowed.app_error", + "translation": "You are not allowed to update the name, display_name, and purpose of direct or group messages." + }, { "id": "api.channel.update_channel_member_roles.changing_guest_role.app_error", "translation": "Invalid channel member update: You can't add or remove the guest role manually." @@ -910,6 +926,10 @@ "id": "api.command_invite.channel.error", "translation": "Could not find the channel {{.Channel}}. Please use the [channel handle](https://docs.mattermost.com/messaging/managing-channels.html#naming-a-channel) to identify channels." }, + { + "id": "api.command_invite.channel_constrained_user_denied", + "translation": "This channel is managed by groups. This user is not part of a group that is synced to this channel." + }, { "id": "api.command_invite.desc", "translation": "Invite a user to a channel" @@ -922,10 +942,6 @@ "id": "api.command_invite.fail.app_error", "translation": "An error occurred while joining the channel." }, - { - "id": "api.command_invite.group_constrained_user_denied", - "translation": "This channel is managed by groups. This user is not part of a group that is synced to this channel." - }, { "id": "api.command_invite.hint", "translation": "@[username]... ~[channel]..." @@ -954,13 +970,28 @@ "id": "api.command_invite.success", "translation": "{{.User}} added to {{.Channel}} channel." }, + { + "id": "api.command_invite.successOverflow", + "translation": "{{.FirstUser}} and {{.Others}} added to {{.Channel}} channel." + }, { "id": "api.command_invite.user_already_in_channel.app_error", - "translation": "{{.User}} is already in the channel." + "translation": { + "one": "{{.User}} is already in the channel.", + "other": "{{.User}} are already in the channel." + } + }, + { + "id": "api.command_invite.user_already_in_channel.overflow", + "translation": "{{.FirstUser}} and {{.Others}} others are already in the channel." }, { "id": "api.command_invite.user_not_in_team.app_error", - "translation": "@{{.Username}} is not a member of the team." + "translation": "You can add {{.Users}} to this channel once they are members of the **{{.Team}}** team." + }, + { + "id": "api.command_invite.user_not_in_team.messageOverflow", + "translation": "You can add {{.FirstUser}} and {{.Others}} others to this channel once they are members of the **{{.Team}}** team." }, { "id": "api.command_invite_people.permission.app_error", @@ -2391,10 +2422,6 @@ "other": "{{.Count}} images sent: {{.Filenames}}" } }, - { - "id": "api.post.link_preview_disabled.app_error", - "translation": "Link previews have been disabled by the system administrator." - }, { "id": "api.post.patch_post.can_not_update_post_in_deleted.error", "translation": "Can not update a post in a deleted channel." @@ -4291,6 +4318,10 @@ "id": "api.user.login.not_verified.app_error", "translation": "Login failed because email address has not been verified." }, + { + "id": "api.user.login.remote_users.login.error", + "translation": "Login failed because remote users are not allow to log in." + }, { "id": "api.user.login.use_auth_service.app_error", "translation": "Please sign in using {{.AuthService}}." @@ -4783,6 +4814,18 @@ "id": "app.channel.get_channels_member_count.find.app_error", "translation": "Unable to find member count." }, + { + "id": "app.channel.get_channels_with_unreads_and_with_mentions.app_error", + "translation": "Unable to check unreads and mentions" + }, + { + "id": "app.channel.get_common_teams.incorrect_channel_type", + "translation": "Channel is not a group message." + }, + { + "id": "app.channel.get_common_teams.store_get_common_teams_error", + "translation": "Couldn't fetch common teams." + }, { "id": "app.channel.get_deleted.existing.app_error", "translation": "Unable to find the existing deleted channel." @@ -4843,6 +4886,26 @@ "id": "app.channel.get_unread.app_error", "translation": "Unable to get the channel unread messages." }, + { + "id": "app.channel.gm_conversion_set_categories.delete_all.error", + "translation": "Couldn't delete existing sidebar categories for converted GM." + }, + { + "id": "app.channel.group_message_conversion.channel_member_missing", + "translation": "Cannot find user's channel membership" + }, + { + "id": "app.channel.group_message_conversion.incorrect_team", + "translation": "The specified team ID in conversion request does not contain all the group message members" + }, + { + "id": "app.channel.group_message_conversion.original_channel_not_gm", + "translation": "The channel being converted is not a group message. You can only convert group messages" + }, + { + "id": "app.channel.group_message_conversion.post_message.error", + "translation": "Failed to create group message to channel conversion post" + }, { "id": "app.channel.migrate_channel_members.select.app_error", "translation": "Failed to select the batch of channel members." @@ -5075,6 +5138,18 @@ "id": "app.custom_group.unique_name", "translation": "group name is not unique" }, + { + "id": "app.desktop_token.generateServerToken.invalid_or_expired", + "translation": "Token does not exist or is expired" + }, + { + "id": "app.desktop_token.validate.invalid", + "translation": "Token is not valid or is expired" + }, + { + "id": "app.desktop_token.validate.no_user", + "translation": "Cannot find a user for this token" + }, { "id": "app.draft.delete.app_error", "translation": "Unable to delete the Draft." @@ -5231,6 +5306,10 @@ "id": "app.file_info.save.app_error", "translation": "Unable to save the file info." }, + { + "id": "app.file_info.set_searchable_content.app_error", + "translation": "Unable to set the searchable content of the file." + }, { "id": "app.group.crud_permission", "translation": "Unable to perform operation for that source type." @@ -5299,6 +5378,10 @@ "id": "app.import.get_users_by_username.some_users_not_found.error", "translation": "Some users not found" }, + { + "id": "app.import.import_channel.deleting.app_error", + "translation": "Unable to archive imported channel." + }, { "id": "app.import.import_channel.scheme_deleted.error", "translation": "Unable to set a channel to use a deleted scheme." @@ -6375,6 +6458,10 @@ "id": "app.reaction.get_for_post.app_error", "translation": "Unable to get reactions for post." }, + { + "id": "app.reaction.permanent_delete_by_user.app_error", + "translation": "Unable to delete reactions for user." + }, { "id": "app.reaction.save.save.app_error", "translation": "Unable to save reaction." @@ -6743,6 +6830,10 @@ "id": "app.terms_of_service.get.no_rows.app_error", "translation": "No terms of service found." }, + { + "id": "app.thread.mark_all_as_read_by_channels.app_error", + "translation": "Unable to mark all threads as read by channel" + }, { "id": "app.update_error", "translation": "update error" @@ -8027,6 +8118,10 @@ "id": "groups.unsupported_syncable_type", "translation": "Unsupported syncable type '{{.Value}}'." }, + { + "id": "humanize.list_join", + "translation": "{{.OtherItems}} and {{.LastItem}}" + }, { "id": "import_process.worker.do_job.file_exists", "translation": "Unable to process import: file does not exists." @@ -8615,6 +8710,10 @@ "id": "model.config.is_valid.ldap_username", "translation": "AD/LDAP field \"Username Attribute\" is required." }, + { + "id": "model.config.is_valid.link_metadata_timeout.app_error", + "translation": "Invalid value for link metadata timeout. Must be a positive number." + }, { "id": "model.config.is_valid.listen_address.app_error", "translation": "Invalid listen address for service settings Must be set." @@ -8835,6 +8934,10 @@ "id": "model.config.is_valid.tls_overwrite_cipher.app_error", "translation": "Invalid value passed for TLS overwrite cipher - Please refer to the documentation for valid values." }, + { + "id": "model.config.is_valid.user_status_away_timeout.app_error", + "translation": "Invalid value for user status away timeout. Must be a positive number." + }, { "id": "model.config.is_valid.webserver_security.app_error", "translation": "Invalid value for webserver connection security." @@ -9785,11 +9888,11 @@ }, { "id": "web.error.unsupported_browser.min_browser_version.chrome", - "translation": "Version 112+" + "translation": "Version 116+" }, { "id": "web.error.unsupported_browser.min_browser_version.edge", - "translation": "Version 112+" + "translation": "Version 116+" }, { "id": "web.error.unsupported_browser.min_browser_version.firefox", diff --git a/server/i18n/en_AU.json b/server/i18n/en_AU.json index 2fe9fe9c8f9..cd69a9eb714 100644 --- a/server/i18n/en_AU.json +++ b/server/i18n/en_AU.json @@ -77,7 +77,7 @@ }, { "id": "web.error.unsupported_browser.min_browser_version.edge", - "translation": "Version 110+" + "translation": "Version 112+" }, { "id": "web.error.unsupported_browser.min_browser_version.chrome", @@ -803,10 +803,6 @@ "id": "model.file_info.is_valid.create_at.app_error", "translation": "Invalid value for create_at." }, - { - "id": "model.file_info.get.gif.app_error", - "translation": "Could not decode GIF." - }, { "id": "model.emoji.user_id.app_error", "translation": "Invalid creator ID." @@ -9118,10 +9114,6 @@ "id": "api.elasticsearch.test_elasticsearch_settings_nil.app_error", "translation": "Elasticsearch settings has unset values." }, - { - "id": "app.recent_searches.app_error", - "translation": "An error occurred while fetching recent searches" - }, { "id": "api.file.cloud_upload.app_error", "translation": "Uploading via mmctl to a Cloud instance is not supported. Please check the documentation here: https://docs.mattermost.com/manage/cloud-data-export.html." @@ -9150,22 +9142,6 @@ "id": "model.config.is_valid.image_decoder_concurrency.app_error", "translation": "Invalid decoder concurrency {{.Value}}. Should be a positive number or -1." }, - { - "id": "app.post.get_top_threads_for_user_since.app_error", - "translation": "Unable to retrieve top threads for user." - }, - { - "id": "app.post.get_top_threads_for_team_since.app_error", - "translation": "Unable to retrieve top threads for team." - }, - { - "id": "app.insights.feature_disabled", - "translation": "Insights feature is disabled." - }, - { - "id": "api.insights.feature_disabled", - "translation": "Insights is behind a feature flag which is not enabled." - }, { "id": "api.cloud.notify_admin_to_upgrade_error.already_notified", "translation": "Already notified admin" @@ -9206,10 +9182,6 @@ "id": "api.templates.delinquency_14.subtitle2", "translation": "Please contact your financial department or institution to resolve any issues. Then, update your payment details as needed." }, - { - "id": "app.post.get_top_dms_for_user_since.app_error", - "translation": "Unable to get top DMs for user." - }, { "id": "app.cloud.get_subscription_delinquency_date.app_error", "translation": "Subscription is not delinquent" @@ -9470,14 +9442,6 @@ "id": "api.team.invite_guests_to_channels.disabled.error", "translation": "Guest accounts are disabled" }, - { - "id": "app.collection.add_topic.exists.app_error", - "translation": "Topic type already exists." - }, - { - "id": "app.collection.add_collection.exists.app_error", - "translation": "Collection type already exists." - }, { "id": "api.user.add_user_to_group_syncables.not_ldap_user.app_error", "translation": "not an LDAP user" @@ -9574,22 +9538,6 @@ "id": "api.acknowledgement.delete.archived_channel.app_error", "translation": "You cannot remove an acknowledgment in an archived channel." }, - { - "id": "worktemplate.product_teams.feature_release.description.playbook", - "translation": "Create transparent workflows across development teams to ensure your feature development process is seamless." - }, - { - "id": "worktemplate.product_teams.feature_release.description.integration", - "translation": "Increase productivity in your channel by integrating a Jira bot and GitHub bot. These will be downloaded for you." - }, - { - "id": "worktemplate.product_teams.feature_release.description.channel", - "translation": "Chat with your team in a Feature Release channel that connects easily with your boards, playbooks and app bots." - }, - { - "id": "worktemplate.category.product_teams", - "translation": "Product" - }, { "id": "model.draft.is_valid.priority.app_error", "translation": "Invalid priority" @@ -9634,14 +9582,6 @@ "id": "ent.elasticsearch.create_client.ca_cert_malformed", "translation": "Decoding of the CA for Elasticsearch failed" }, - { - "id": "app.worktemplates.get_templates.app_error", - "translation": "Unable to retrieve work templates" - }, - { - "id": "app.worktemplates.get_categories.app_error", - "translation": "Unable to retrieve categories for work templates" - }, { "id": "app.channel.count_urgent_posts_since.app_error", "translation": "Unable to enumerate urgent posts since the given date." @@ -9698,66 +9638,6 @@ "id": "api.acknowledgement.delete.deadline.app_error", "translation": "You cannot delete an acknowledgment after 5 minutes has elapsed." }, - { - "id": "worktemplate.category.leadership", - "translation": "Leadership" - }, - { - "id": "worktemplate.category.devops", - "translation": "DevOps" - }, - { - "id": "worktemplate.category.companywide", - "translation": "Company-wide" - }, - { - "id": "app.worktemplates.execute_work_template.playbooks.find_channel_error", - "translation": "Unable to find channel associated with a playbook." - }, - { - "id": "app.worktemplates.execute_work_template.playbooks.create_error", - "translation": "Error while creating a playbook." - }, - { - "id": "app.worktemplates.execute_work_template.channels.create_error", - "translation": "Error while creating a channel." - }, - { - "id": "app.worktemplates.execute_work_template.boards.create_error", - "translation": "Error while creating a board." - }, - { - "id": "app.worktemplates.execute_work_template.app_error", - "translation": "Error while executing a work template." - }, - { - "id": "app.worktemplate.execution_request.license_cannot_create_private_playbook", - "translation": "Your licence does not support private playbooks." - }, - { - "id": "app.worktemplate.execution_request.cannot_create_public_playbook", - "translation": "You don't have permission to create a public playbook." - }, - { - "id": "app.worktemplate.execution_request.cannot_create_public_channel", - "translation": "You don't have permission to create a public channel." - }, - { - "id": "app.worktemplate.execution_request.cannot_create_public_board", - "translation": "You don't have permission to create a public board." - }, - { - "id": "app.worktemplate.execution_request.cannot_create_private_playbook", - "translation": "You don't have permission to create a private playbook." - }, - { - "id": "app.worktemplate.execution_request.cannot_create_private_channel", - "translation": "You don't have permission to create a private channel." - }, - { - "id": "app.worktemplate.execution_request.cannot_create_private_board", - "translation": "You don't have permission to create a private board." - }, { "id": "app.import.validate_user_teams_import_data.invalid_auth_service.error", "translation": "Invalid auth service: {{.AuthService}}" @@ -9766,10 +9646,6 @@ "id": "app.group.username_conflict", "translation": "A user with the username '{{.Username}}' already exists." }, - { - "id": "api.work_templates.disabled", - "translation": "Work templates are disabled." - }, { "id": "api.server.cws.needs_enterprise_edition", "translation": "Service only available in Mattermost Enterprise edition" @@ -9778,46 +9654,6 @@ "id": "api.config.update_config.translations.app_error", "translation": "Failed to update server translations." }, - { - "id": "api.command_templates.unsupported.app_error", - "translation": "The templates command is not supported on your device." - }, - { - "id": "api.command_templates.name", - "translation": "templates" - }, - { - "id": "api.command_templates.desc", - "translation": "Open the create from template window" - }, - { - "id": "app.worktemplates.execute_work_template.name_too_long", - "translation": "The name field cannot contain more than 64 characters." - }, - { - "id": "app.team.create_onboarding_linked_board.product_key_not_found", - "translation": "product key not found" - }, - { - "id": "app.team.create_onboarding_linked_board.itf_not_of_type", - "translation": "interface not of expected type" - }, - { - "id": "app.team.create_onboarding_linked_board.error_no_board", - "translation": "error no board created" - }, - { - "id": "app.team.create_onboarding_linked_board.error_patching_board", - "translation": "error while patching board" - }, - { - "id": "app.team.create_onboarding_linked_board.error_getting_templates", - "translation": "error getting boards templates" - }, - { - "id": "app.team.create_onboarding_linked_board.error_duplicating_board", - "translation": "error duplicating board" - }, { "id": "api.system.logs.invalidFilter", "translation": "Invalid log filter" @@ -9830,251 +9666,10 @@ "id": "api.templates.license_up_for_renewal_contact_sales", "translation": "Contact Sales" }, - { - "id": "worktemplate.product_teams.sprint_planning.integration", - "translation": "Increase productivity in your channel by integrating your most commonly used tools such as Zoom. These will be downloaded for you." - }, - { - "id": "worktemplate.product_teams.sprint_planning.channel", - "translation": "Chat with your team in a channel that connects easily with your boards and integrations." - }, - { - "id": "worktemplate.product_teams.sprint_planning.board", - "translation": "Track your team's progress toward weekly goals with sprint breakdowns, prioritisation, owner assignment and comments." - }, - { - "id": "worktemplate.product_teams.product_roadmap.channel", - "translation": "Chat with your team about your customers' feedback, prioritisation and get aligned on progress together." - }, - { - "id": "worktemplate.product_teams.product_roadmap.board", - "translation": "Use the Product Roadmap board to manage user feedback, assign resources, view deliverables in a calendar view and prioritise issues." - }, - { - "id": "worktemplate.product_teams.goals_and_okrs.integration", - "translation": "Increase productivity in your channel by integrating your most commonly used tools such as Zoom to facilitate easy collaboration. These will be downloaded for you." - }, - { - "id": "worktemplate.product_teams.goals_and_okrs.channel", - "translation": "Chat about your goals and progress with your team, async or real-time and stay up to date with changes in a single channel." - }, - { - "id": "worktemplate.product_teams.goals_and_okrs.board", - "translation": "Track your team's progress toward organisational goals with the Goals and OKR board. Keep meetings on track with the Meeting Agenda board." - }, - { - "id": "worktemplate.product_teams.feature_release.description.board", - "translation": "Keep meetings on track with the Meeting Agenda board. Manage your workload with the Project Tasks board." - }, - { - "id": "worktemplate.product_teams.bug_bash.playbook", - "translation": "Use checklists to assign testing areas and automated tasks to run a comprehensive bug bash process. Use a retrospective to review your process and improve it for next time." - }, - { - "id": "worktemplate.product_teams.bug_bash.integration", - "translation": "Increase productivity in your channel by integrating your most commonly used tools such as Jira to track your bug bash progress. These will be downloaded for you." - }, - { - "id": "worktemplate.product_teams.bug_bash.channel", - "translation": "Plan and manage bug reports and resolutions in a single channel that’s easily accessible to your team and organisation." - }, - { - "id": "worktemplate.leadership.goals_and_okrs.integration", - "translation": "Increase productivity in your channel by integrating your most commonly used tools such as Zoom to facilitate easy collaboration. These will be downloaded for you." - }, - { - "id": "worktemplate.leadership.goals_and_okrs.channel", - "translation": "Chat about your goals and progress with your team, async or real-time and stay up to date with changes in a single channel." - }, - { - "id": "worktemplate.leadership.goals_and_okrs.board", - "translation": "Track your team's progress toward organisational goals with the Goals and OKR board. Keep meetings on track with the Meeting Agenda board." - }, - { - "id": "worktemplate.devops.product_release.playbook", - "translation": "Create repeatable workflows that are easy to follow and implement so product releases are reliable and on time." - }, - { - "id": "worktemplate.devops.product_release.channel", - "translation": "Chat with your team about daily milestones, any blockers and changes to deliverables, easily and quickly." - }, - { - "id": "worktemplate.devops.product_release.board", - "translation": "Use the Product Release board to support your release timeframe and process, ensuring everyone knows which tasks are due." - }, - { - "id": "worktemplate.devops.incident_resolution.description.playbook", - "translation": "Use checklists and automation to bring in key team members and share how your incident is tracking toward resolution." - }, - { - "id": "worktemplate.devops.incident_resolution.description.channel", - "translation": "Chat with your team about priorities, add stakeholders, provide updates and work toward a resolution in a single channel." - }, - { - "id": "worktemplate.devops.incident_resolution.description.board", - "translation": "Use the Incident Resolution board to support repeatable processes and assign defined tasks across the team." - }, - { - "id": "worktemplate.companywide.goals_and_okrs.integration", - "translation": "Increase productivity in your channel by integrating your most commonly used tools such as Zoom to facilitate easy collaboration. These will be downloaded for you." - }, - { - "id": "worktemplate.companywide.goals_and_okrs.channel", - "translation": "Chat about your goals and progress with your team, async or real-time and stay up to date with changes in a single channel." - }, - { - "id": "worktemplate.companywide.goals_and_okrs.board", - "translation": "Track your team's progress toward organisational goals with the Goals and OKR board. Keep meetings on track with the Meeting Agenda board." - }, - { - "id": "worktemplate.companywide.create_project.integration", - "translation": "Increase productivity in your channel by integrating your most commonly used tools. These will be downloaded for you." - }, - { - "id": "worktemplate.companywide.create_project.channel", - "translation": "Chat with your team about your new project and decide how you’re going to structure it in a collaborative channel." - }, - { - "id": "worktemplate.companywide.create_project.board", - "translation": "Use a Kanban board to define and track your project tasks and progress." - }, { "id": "model.license_record.is_valid.bytes.app_error", "translation": "Invalid value for bytes when uploading a licence." }, - { - "id": "app.user.run.update_status.title", - "translation": "Status update" - }, - { - "id": "app.user.run.update_status.submit_label", - "translation": "Update status" - }, - { - "id": "app.user.run.update_status.reminder_for_next_update", - "translation": "Reminder for next update" - }, - { - "id": "app.user.run.update_status.num_channel", - "translation": { - "one": "Provide an update to the stakeholders. This post will be broadcasted to {{.Count}} channel.", - "other": "Provide an update to the stakeholders. This post will be broadcasted to {{.Count}} channels." - } - }, - { - "id": "app.user.run.update_status.finish_run.placeholder", - "translation": "Also mark the run as finished" - }, - { - "id": "app.user.run.update_status.finish_run", - "translation": "Finish run" - }, - { - "id": "app.user.run.update_status.change_since_last_update", - "translation": "Change since last update" - }, - { - "id": "app.user.run.status_enable", - "translation": "@{{.Username}} enabled the status updates for [{{.RunName}}]({{.RunURL}})" - }, - { - "id": "app.user.run.status_disable", - "translation": "@{{.Username}} disabled the status updates for [{{.RunName}}]({{.RunURL}})" - }, - { - "id": "app.user.run.request_update", - "translation": "@here — @{{.Name}} requested a status update for [{{.RunName}}]({{.RunURL}}). \n" - }, - { - "id": "app.user.run.request_join_channel", - "translation": "@{{.Name}} is a run participant and wants join this channel. Any member of the channel can invite them.\n" - }, - { - "id": "app.user.run.confirm_finish.title", - "translation": "Confirm finish run" - }, - { - "id": "app.user.run.confirm_finish.submit_label", - "translation": "Finish run" - }, - { - "id": "app.user.run.confirm_finish.num_outstanding", - "translation": { - "one": "There is **{{.Count}} outstanding task**. Are you sure you want to finish the run *{{.RunName}}* for all participants?", - "other": "There are **{{.Count}} outstanding tasks**. Are you sure you want to finish the run *{{.RunName}}* for all participants?" - } - }, - { - "id": "app.user.run.add_to_timeline.title", - "translation": "Add to run timeline" - }, - { - "id": "app.user.run.add_to_timeline.summary.placeholder", - "translation": "Short summary shown in the timeline" - }, - { - "id": "app.user.run.add_to_timeline.summary.help", - "translation": "Max 64 characters" - }, - { - "id": "app.user.run.add_to_timeline.summary", - "translation": "Summary" - }, - { - "id": "app.user.run.add_to_timeline.submit_label", - "translation": "Add to run timeline" - }, - { - "id": "app.user.run.add_to_timeline.playbook_run", - "translation": "Playbook Run" - }, - { - "id": "app.user.run.add_checklist_item.title", - "translation": "Add new task" - }, - { - "id": "app.user.run.add_checklist_item.submit_label", - "translation": "Add task" - }, - { - "id": "app.user.run.add_checklist_item.name", - "translation": "Name" - }, - { - "id": "app.user.run.add_checklist_item.description", - "translation": "Description" - }, - { - "id": "app.user.new_run.title", - "translation": "Run playbook" - }, - { - "id": "app.user.new_run.submit_label", - "translation": "Start run" - }, - { - "id": "app.user.new_run.run_name", - "translation": "Run name" - }, - { - "id": "app.user.new_run.playbook", - "translation": "Playbook" - }, - { - "id": "app.user.new_run.intro", - "translation": "**Owner** {{.Username}}" - }, - { - "id": "app.user.digest.tasks.zero_assigned", - "translation": "You have 0 assigned tasks." - }, - { - "id": "app.user.digest.tasks.num_assigned_due_until_today", - "translation": { - "one": "You have {{.Count}} assigned task that is now due:", - "other": "You have {{.Count}} assigned tasks that are now due:" - } - }, { "id": "app.user.digest.tasks.num_assigned", "translation": { @@ -10082,78 +9677,10 @@ "other": "You have {{.Count}} total assigned tasks:" } }, - { - "id": "app.user.digest.tasks.heading", - "translation": "Your assigned tasks" - }, - { - "id": "app.user.digest.tasks.due_yesterday", - "translation": "Due yesterday" - }, - { - "id": "app.user.digest.tasks.due_x_days_ago", - "translation": "Due {{.Count}} days ago" - }, - { - "id": "app.user.digest.tasks.due_today", - "translation": "Due today" - }, - { - "id": "app.user.digest.tasks.due_in_x_days", - "translation": { - "one": "Due in {{.Count}} day", - "other": "Due in {{.Count}} days" - } - }, - { - "id": "app.user.digest.tasks.due_after_today", - "translation": { - "one": "You have **{{.Count}} assigned task due after today**.", - "other": "You have **{{.Count}} assigned tasks due after today**." - } - }, - { - "id": "app.user.digest.tasks.all_tasks_command", - "translation": "Please use `/playbook todo` to see all your tasks." - }, - { - "id": "app.user.digest.runs_in_progress.zero_in_progress", - "translation": "You have 0 runs currently in progress." - }, - { - "id": "app.user.digest.runs_in_progress.num_in_progress", - "translation": { - "one": "You have {{.Count}} run currently in progress:", - "other": "You have {{.Count}} runs currently in progress:" - } - }, - { - "id": "app.user.digest.runs_in_progress.heading", - "translation": "Runs in Progress" - }, - { - "id": "app.user.digest.overdue_status_updates.zero_overdue", - "translation": "You have 0 runs overdue." - }, - { - "id": "app.user.digest.overdue_status_updates.num_overdue", - "translation": { - "one": "You have {{.Count}} run overdue for a status update:", - "other": "You have {{.Count}} runs overdue for a status update:" - } - }, - { - "id": "app.user.digest.overdue_status_updates.heading", - "translation": "Overdue Status Updates" - }, { "id": "app.oauth.remove_auth_data_by_client_id.app_error", "translation": "Unable to remove OAuth data." }, - { - "id": "app.command.execute.error", - "translation": "Unable to execute command." - }, { "id": "api.server.cws.subscribe_to_newsletter.app_error", "translation": "CWS Server failed to subscribe to newsletter." @@ -10161,5 +9688,189 @@ { "id": "api.license.request-trial.bad-request.business-email", "translation": "Invalid business email for trial" + }, + { + "id": "model.config.is_valid.elastic_search.ignored_indexes_dash_prefix.app_error", + "translation": "Ignored indexes for purge should not start with dash." + }, + { + "id": "app.save_config.plugin_hook_error", + "translation": "An error occurred running the plugin hook on configuration save." + }, + { + "id": "api.user.invalidate_password_recovery_tokens_delete.error", + "translation": "Unable to remove token when invalidating password recovery tokens" + }, + { + "id": "ent.elasticsearch.max_version.app_error", + "translation": "Elasticsearch version {{.Version}} is higher than max supported version of {{.MaxVersion}}" + }, + { + "id": "model.config.is_valid.local_mode_socket.app_error", + "translation": "Unable to locate local socket file directory." + }, + { + "id": "api.post.post_priority.priority_post_not_allowed_for_user.request_error", + "translation": "User is not allowed to create priority post or persistent notification." + }, + { + "id": "api.post.post_priority.priority_post_only_allowed_for_root_post.request_error", + "translation": "Only root posts are allowed to have priority." + }, + { + "id": "api.user.invalidate_password_recovery_tokens_parse.error", + "translation": "Unable to parse token when invalidating password recovery tokens" + }, + { + "id": "app.post_priority.delete_persistent_notification_post.app_error", + "translation": "Failed to delete persistent notification post" + }, + { + "id": "api.error_no_organization_name_provided_for_self_hosted_onboarding", + "translation": "Error: no organisation name provided for self hosted onboarding." + }, + { + "id": "app.file_info.get.gif.app_error", + "translation": "Could not decode gif." + }, + { + "id": "app.plugin.skip_installation.app_error", + "translation": "Skipping installation of plugin {{.Id}} since existing version is equal or newer." + }, + { + "id": "api.license.true_up_review.failed_to_submit", + "translation": "Failed to submit true up review profile to CWS." + }, + { + "id": "api.post.post_priority.max_recipients_persistent_notification_post.request_error", + "translation": "Persistent notification post allows maximum of {{.MaxRecipients}} recipients." + }, + { + "id": "api.post.post_priority.min_recipients_persistent_notification_post.request_error", + "translation": "Persistent notification post must have at minimum 1 recipient." + }, + { + "id": "api.command_exportlink.desc", + "translation": "Generate a link to download a export." + }, + { + "id": "api.command_exportlink.driver.app_error", + "translation": "The file store driver does not support link generation." + }, + { + "id": "api.command_exportlink.empty.app_error", + "translation": "No export file has been found." + }, + { + "id": "api.command_exportlink.hint", + "translation": "[job-id|zip filename|{{.LatestMsg}}]" + }, + { + "id": "api.command_exportlink.invalid.app_error", + "translation": "Unable to locate the file you requested." + }, + { + "id": "api.command_exportlink.link.text", + "translation": "You can download your file here: {{.Link}}.\nThis link will expire in {{.Expiration}}." + }, + { + "id": "api.command_exportlink.list.app_error", + "translation": "Unable to retrieve the export list." + }, + { + "id": "api.command_exportlink.name", + "translation": "exportlink" + }, + { + "id": "api.command_exportlink.permission.app_error", + "translation": "You don't have the correct permissions to run this command." + }, + { + "id": "api.command_exportlink.presign.app_error", + "translation": "Unable to generate presign URL." + }, + { + "id": "app.channel.get_channels_member_count.existing.app_error", + "translation": "Unable to find member count for given channels." + }, + { + "id": "app.channel.get_channels_member_count.find.app_error", + "translation": "Unable to find member count." + }, + { + "id": "app.channel.update_member.app_error", + "translation": "Unable to update the channel member." + }, + { + "id": "app.channel.update_member.notify_props_limit_exceeded.app_error", + "translation": "Unable to update the channel member, notify props size limit exceeded." + }, + { + "id": "app.eport.generate_presigned_url.config.app_error", + "translation": "This action requires the use of a dedicated export store." + }, + { + "id": "app.eport.generate_presigned_url.driver.app_error", + "translation": "Your export store driver does not support presign URL generation." + }, + { + "id": "app.eport.generate_presigned_url.featureflag.app_error", + "translation": "This feature is restricted by a feature flag." + }, + { + "id": "app.eport.generate_presigned_url.fileexist.app_error", + "translation": "Unable to check if the file exists." + }, + { + "id": "app.eport.generate_presigned_url.link.app_error", + "translation": "Unable to generate the presigned URL." + }, + { + "id": "app.eport.generate_presigned_url.notfound.app_error", + "translation": "The export file was not found." + }, + { + "id": "model.channel_member.is_valid.notify_props.app_error", + "translation": "Notify props size limit exceeded." + }, + { + "id": "model.config.is_valid.persistent_notifications_recipients.app_error", + "translation": "Invalid maximum number of recipients for persistent notifications. Must be a positive number." + }, + { + "id": "api.post.post_priority.persistent_notification_validation_error.request_error", + "translation": "Persistent notification validation failed." + }, + { + "id": "api.post.post_priority.urgent_persistent_notification_post.request_error", + "translation": "Persistent notification posts must have the Urgent Priority." + }, + { + "id": "api.user.invalidate_password_recovery_tokens.error", + "translation": "Unable to get tokens by type when invalidating password recovery tokens" + }, + { + "id": "app.post.delete_post.get_team.app_error", + "translation": "An error occurred getting the team." + }, + { + "id": "app.post_persistent_notification.delete_by_channel.app_error", + "translation": "Unable to delete the persistent notifications by channel." + }, + { + "id": "app.post_persistent_notification.delete_by_team.app_error", + "translation": "Unable to delete the persistent notifications by team." + }, + { + "id": "model.channel_member.is_valid.channel_auto_follow_threads_value.app_error", + "translation": "Invalid channel-auto-follow-threads value." + }, + { + "id": "model.config.is_valid.persistent_notifications_count.app_error", + "translation": "Invalid total number of persistent notification per post. Must be a positive number." + }, + { + "id": "model.config.is_valid.persistent_notifications_interval.app_error", + "translation": "Invalid frequency of persistent notifications. Must be at least two minutes." } ] diff --git a/server/i18n/es.json b/server/i18n/es.json index 3b8f7976217..0f3879a3eb9 100644 --- a/server/i18n/es.json +++ b/server/i18n/es.json @@ -4043,10 +4043,6 @@ "id": "model.emoji.user_id.app_error", "translation": "Id del creador inválido." }, - { - "id": "model.file_info.get.gif.app_error", - "translation": "No se pudo decodificar el gif." - }, { "id": "model.file_info.is_valid.create_at.app_error", "translation": "Valor no válido para create_at." @@ -8987,10 +8983,6 @@ "id": "common.parse_error_int64", "translation": " " }, - { - "id": "app.post.get_top_threads_for_user_since.app_error", - "translation": "No se pudo obtener los hilos principales para el usuario." - }, { "id": "app.post.cloud.get.app_error", "translation": "No se puede recuperar la publicación porque ha superado el límite del plan de la nube." @@ -8999,10 +8991,6 @@ "id": "app.last_accessible_post.app_error", "translation": " " }, - { - "id": "app.post.get_top_threads_for_team_since.app_error", - "translation": "No fue posible obtener los hilos pricipales para el equipo." - }, { "id": "api.cloud.notify_admin_to_upgrade_error.already_notified", "translation": " " @@ -9123,22 +9111,10 @@ "id": "model.config.is_valid.image_decoder_concurrency.app_error", "translation": "Decodificador de concurrencia inválido {{.Value}}. Debe ser un número positivo o -1." }, - { - "id": "app.insights.feature_disabled", - "translation": "La característica Perspectivas está deshabilitada." - }, - { - "id": "api.insights.feature_disabled", - "translation": "Las Perspectivas están detrás de una bandera de ajuste que no está habilitada." - }, { "id": "model.member.is_valid.emails.app_error", "translation": " " }, - { - "id": "app.recent_searches.app_error", - "translation": " " - }, { "id": "app.system.complete_onboarding_request.no_first_user", "translation": "La inducción sólo puede ser completada por un Administrador del Sistema." @@ -9431,10 +9407,6 @@ "id": "app.post_reminder_dm", "translation": "Hola, este es un recordatorio acerca de este mensaje de @{{.Username}}: {{.SiteURL}}/{{.TeamName}}/pl/{{.PostId}}" }, - { - "id": "app.post.get_top_dms_for_user_since.app_error", - "translation": "No es posible obtener los DMs principales para el usuario." - }, { "id": "app.notify_admin.send_notification_post.app_error", "translation": "No es posible enviar la publicación de notificación." @@ -9467,14 +9439,6 @@ "id": "app.cloud.trial_plan_bot_message_single", "translation": "el miembro {{.UsersNum}} del workspace {{.WorkspaceName}} ha solicitado comenzar la prueba Enterprise para acceder a: " }, - { - "id": "app.collection.add_topic.exists.app_error", - "translation": "El tipo de tema ya existe." - }, - { - "id": "app.collection.add_collection.exists.app_error", - "translation": "El tipo de colección ya existe." - }, { "id": "api.user.add_user_to_group_syncables.not_ldap_user.app_error", "translation": "no es un usuario ldap" @@ -9486,9 +9450,5 @@ { "id": "api.admin.syncables_error", "translation": "Error al agregar usuario a grupo-equipos y grupo-canales" - }, - { - "id": "api.command_templates.name", - "translation": "plantillas" } ] diff --git a/server/i18n/et.json b/server/i18n/et.json new file mode 100644 index 00000000000..de7be08fa9d --- /dev/null +++ b/server/i18n/et.json @@ -0,0 +1,78 @@ +[ + { + "id": "api.user.upload_profile_user.open.app_error", + "translation": "Sertifikaadi faili avamine nurjus." + }, + { + "id": "api.team.set_team_icon.open.app_error", + "translation": "Sertifikaadi faili avamine nurjus." + }, + { + "id": "api.team.import_team.open.app_error", + "translation": "Sertifikaadi faili avamine nurjus." + }, + { + "id": "api.license.add_license.open.app_error", + "translation": "Sertifikaadi faili avamine nurjus." + }, + { + "id": "api.admin.saml.failure_save_idp_certificate_file.app_error", + "translation": "Sertifikaadi faili avamine nurjus." + }, + { + "id": "api.admin.add_certificate.saving.app_error", + "translation": "Sertifikaadi faili avamine nurjus." + }, + { + "id": "api.admin.add_certificate.open.app_error", + "translation": "Sertifikaadi faili avamine nurjus." + }, + { + "id": "September", + "translation": "September" + }, + { + "id": "October", + "translation": "Oktoober" + }, + { + "id": "November", + "translation": "November" + }, + { + "id": "May", + "translation": "Mai" + }, + { + "id": "March", + "translation": "Märts" + }, + { + "id": "June", + "translation": "Juuni" + }, + { + "id": "July", + "translation": "Juuli" + }, + { + "id": "January", + "translation": "Jaanuar" + }, + { + "id": "February", + "translation": "Veebruar" + }, + { + "id": "December", + "translation": "Detsember" + }, + { + "id": "August", + "translation": "August" + }, + { + "id": "April", + "translation": "Aprill" + } +] diff --git a/server/i18n/eu.json b/server/i18n/eu.json new file mode 100644 index 00000000000..a072d0e6421 --- /dev/null +++ b/server/i18n/eu.json @@ -0,0 +1,174 @@ +[ + { + "id": "api.channel.add_user_to_channel.type.app_error", + "translation": "Ezin da erabiltzailerik gehitu kanal mota honetan." + }, + { + "id": "api.channel.add_user.to.channel.failed.deleted.app_error", + "translation": "Ezin izan da erabiltzailea kanalean gehitu taldetik ezabatu dutelako." + }, + { + "id": "api.channel.add_user.to.channel.failed.app_error", + "translation": "Erabiltzailea ezin izan da kanalean gehitu." + }, + { + "id": "api.channel.add_members.user_denied", + "translation": "Kanalaren partaidetza ukatu egin zaie hurrengo erabiltzaileei taldearen murrizketengatik: {{ .UserIDs }}" + }, + { + "id": "api.channel.add_members.error", + "translation": "Errorea partaidea(k) kanalera gehitzean." + }, + { + "id": "api.channel.add_member.added", + "translation": "%v gehitu da kanalera %v-k eginda." + }, + { + "id": "api.channel.add_guest.added", + "translation": "%v gehitu da kanalera gonbidatu giza honengatik %v." + }, + { + "id": "api.bot.teams_channels.add_message_mobile", + "translation": "Mesedez gehitu nazazu parte hartzea nahi duzun talde eta kanaletan. Hau egiteko, erabil nabigatzailea edo Mattermost mahaigaineko aplikazioa." + }, + { + "id": "api.bot.create_disabled", + "translation": "Bot-en sorkuntza desgaitu egin da." + }, + { + "id": "api.admin.upload_brand_image.too_large.app_error", + "translation": "Ezin da fitxategia igo. Fitxategia handiegia da." + }, + { + "id": "api.admin.upload_brand_image.storage.app_error", + "translation": "Ezin izan da irudia igo. Irudien biltegia ez da konfiguratu." + }, + { + "id": "api.admin.test_s3.missing_s3_bucket", + "translation": "S3 Bucket behar da" + }, + { + "id": "api.admin.test_email.subject", + "translation": "Mattermost - E-posta ezarpenak probatzen" + }, + { + "id": "api.admin.test_email.reenter_password", + "translation": "SMTP zerbitzaria, ataka, edo erabiltzaile izena aldatu da. Mesedez sartu berriro SMTP pasahitza konexioa probatzeko." + }, + { + "id": "api.admin.test_email.missing_server", + "translation": "SMTP Zerbitzaria beharrezkoa da" + }, + { + "id": "api.admin.test_email.body", + "translation": "Badirudi zure Mattermost e-posta zuzen konfiguratuta dagoela!" + }, + { + "id": "api.admin.saml.set_certificate_from_metadata.missing_content_type.app_error", + "translation": "Eduki mota falta da." + }, + { + "id": "api.admin.saml.set_certificate_from_metadata.invalid_content_type.app_error", + "translation": "Eduki mota baliogabea." + }, + { + "id": "api.admin.saml.set_certificate_from_metadata.invalid_body.app_error", + "translation": "Zertifikatuaren testua ez da baliagarria." + }, + { + "id": "api.admin.saml.not_available.app_error", + "translation": "SAML 2.0 ez da konfiguratu edo onartzen zerbitzari honetan." + }, + { + "id": "api.admin.saml.failure_save_idp_certificate_file.app_error", + "translation": "Ezin izan da gorde zertifikatua." + }, + { + "id": "api.admin.remove_certificate.delete.app_error", + "translation": "Errore bat gertatu da zertifikatua ezabatzean." + }, + { + "id": "api.admin.ldap.not_available.app_error", + "translation": "LDAP ez dago eskuragarri." + }, + { + "id": "api.admin.get_brand_image.storage.app_error", + "translation": "Irudien biltegia ez da konfiguratu." + }, + { + "id": "api.admin.file_read_error", + "translation": "Errorea log fitxategia irakurtzean." + }, + { + "id": "api.admin.delete_brand_image.storage.not_found", + "translation": "Ezin izan da markaren irudia ezabatu, ez da aurkitu." + }, + { + "id": "api.admin.add_certificate.saving.app_error", + "translation": "Ezin izan da gorde zertifikatuaren fitxategia." + }, + { + "id": "api.admin.add_certificate.open.app_error", + "translation": "Ezin izan da ireki zertifikatuaren fitxategia." + }, + { + "id": "api.admin.add_certificate.no_file.app_error", + "translation": "Fitxategirik ez 'zertifikatu' azpian eskaeran." + }, + { + "id": "api.admin.add_certificate.array.app_error", + "translation": "Fitxategirik ez 'zertifikatua' azpian eskaeran" + }, + { + "id": "September", + "translation": "Iraila" + }, + { + "id": "October", + "translation": "Urria" + }, + { + "id": "November", + "translation": "Azaroa" + }, + { + "id": "May", + "translation": "Maiatza" + }, + { + "id": "March", + "translation": "Martxoa" + }, + { + "id": "June", + "translation": "Ekaina" + }, + { + "id": "July", + "translation": "Uztaila" + }, + { + "id": "January", + "translation": "Urtarrila" + }, + { + "id": "February", + "translation": "Otsaila" + }, + { + "id": "December", + "translation": "Abendua" + }, + { + "id": "August", + "translation": "Abuztua" + }, + { + "id": "April", + "translation": "Apirila" + }, + { + "id": "api.channel.channel_member_counts_by_group.license.error", + "translation": "Zure lizentziak ez du talderik onartzen" + } +] diff --git a/server/i18n/fa.json b/server/i18n/fa.json index c9857ac8e03..3be10eddc87 100644 --- a/server/i18n/fa.json +++ b/server/i18n/fa.json @@ -583,10 +583,6 @@ "id": "model.file_info.is_valid.create_at.app_error", "translation": "مقدارcreate_at نامعتبر است." }, - { - "id": "model.file_info.get.gif.app_error", - "translation": "رمزگشایی gif امکان پذیر نیست." - }, { "id": "model.emoji.user_id.app_error", "translation": "شناسه سازنده نامعتبر است." diff --git a/server/i18n/fi.json b/server/i18n/fi.json new file mode 100644 index 00000000000..b0e7f5d1a49 --- /dev/null +++ b/server/i18n/fi.json @@ -0,0 +1,5218 @@ +[ + { + "id": "web.incoming_webhook.user.app_error", + "translation": "Käyttäjää ei löydetty" + }, + { + "id": "web.incoming_webhook.text.app_error", + "translation": "Tekstiä ei määritetty" + }, + { + "id": "web.incoming_webhook.split_props_length.app_error", + "translation": "Ei voitu jakaa webhookin asetuksia {{.Max}} merkkiosaan." + }, + { + "id": "web.incoming_webhook.permissions.app_error", + "translation": "Virheelliset kanavat oikeudet" + }, + { + "id": "web.incoming_webhook.parse.app_error", + "translation": "Ei voitu lukea sisääntulevaa dataa" + }, + { + "id": "web.incoming_webhook.invalid.app_error", + "translation": "Virheellinen webhook" + }, + { + "id": "web.incoming_webhook.disabled.app_error", + "translation": "Pääkäyttäjä on estänyt tulevien webhookien käyttämisen." + }, + { + "id": "web.incoming_webhook.channel_locked.app_error", + "translation": "Tällä webhookilla ei ole lupaa kirjoittaa pyydetylle kanavalle" + }, + { + "id": "web.incoming_webhook.channel.app_error", + "translation": "Ei löydetty kanavaa" + }, + { + "id": "web.get_access_token.internal_saving.app_error", + "translation": "Ei voitu päivittää käyttäjän pääsydataa." + }, + { + "id": "web.command_webhook.parse.app_error", + "translation": "Ei voitu lukea sisääntulevaa dataa" + }, + { + "id": "web.command_webhook.command.app_error", + "translation": "Ei löydetty komentoa" + }, + { + "id": "system.message.name", + "translation": "Järjestelmä" + }, + { + "id": "app.user_terms_of_service.delete.app_error", + "translation": "Ei voitu poistaa palveluehtoja." + }, + { + "id": "app.terms_of_service.create.existing.app_error", + "translation": "Ei saa kutsua tallennusta olemassaolevalle palveluehdolle." + }, + { + "id": "store.sql_team.save_member.exists.app_error", + "translation": "Tiiminjäsen jolla tämä ID on jo olemassa" + }, + { + "id": "app.scheme.save.invalid_scheme.app_error", + "translation": "Annettu järjestely on virheellinen" + }, + { + "id": "app.scheme.save.app_error", + "translation": "Ei voitu luoda järjestelyä" + }, + { + "id": "app.scheme.permanent_delete_all.app_error", + "translation": "Ei voitu pysyvästi poistaa järjestelyjä" + }, + { + "id": "app.scheme.get.app_error", + "translation": "Ei voitu hakea järjestelyä" + }, + { + "id": "app.reaction.save.save.app_error", + "translation": "Ei voitu tallentaa reaktiota" + }, + { + "id": "app.reaction.delete_all_with_emoji_name.get_reactions.app_error", + "translation": "Ei voitu hakea reaktioita joissa tämä emojin nimi" + }, + { + "id": "store.sql_post.search.disabled", + "translation": "Etsintä on kytketty pois tällä palvelimella. Ota yhteys järjestelmänvalvojaan." + }, + { + "id": "app.emoji.delete.no_results", + "translation": "Ei löydetty poistettavaa emojia" + }, + { + "id": "app.emoji.delete.app_error", + "translation": "Ei voitu poistaa emojia" + }, + { + "id": "store.sql_command.save.get.app_error", + "translation": "Ei voitu hakea komentoa" + }, + { + "id": "app.channel_member_history.log_leave_event.internal_error", + "translation": "Ei voitu hakea tallentaa kanavan jäsenhistoriaa. Ei voitu päivittää olemassaolevaa liittymistallennetta" + }, + { + "id": "app.channel_member_history.log_join_event.internal_error", + "translation": "Ei voitu hakea tallentaa kanavan jäsenhistoriaa" + }, + { + "id": "model.config.is_valid.saml_public_cert.app_error", + "translation": "Service Provider Public Certificate puuttuu. Unohditko ladata sen?" + }, + { + "id": "model.config.is_valid.saml_private_key.app_error", + "translation": "Service Provider Private Key puuttuu. Unohditko ladata sen?" + }, + { + "id": "model.config.is_valid.saml_idp_url.app_error", + "translation": "SAML SSO URLin oltava kunnollinen URL ja alettava http:// tai https://." + }, + { + "id": "model.config.is_valid.saml_idp_descriptor_url.app_error", + "translation": "Identity Provider Issuer URLin oltava kunnollinen URL ja alettava http:// tai https://." + }, + { + "id": "model.config.is_valid.saml_idp_cert.app_error", + "translation": "Identity Provider Public Certificate puuttuu. Unohditko ladata sen?" + }, + { + "id": "model.config.is_valid.saml_email_attribute.app_error", + "translation": "Virheellinen Sähköposti attribuutti. Oltava asetettu." + }, + { + "id": "model.config.is_valid.saml_assertion_consumer_service_url.app_error", + "translation": "Service Provider kirjautumis-URLin oltava kunnollinen URL ja alettava http:// tai https://." + }, + { + "id": "model.config.is_valid.restrict_direct_message.app_error", + "translation": "Virheellinen yksityisviestin rajoitus. On oltava 'any' tai 'team'" + }, + { + "id": "model.config.is_valid.read_timeout.app_error", + "translation": "Virheellinen luvun aikakatkaisuarvo." + }, + { + "id": "model.config.is_valid.rate_sec.app_error", + "translation": "Virheellinen per sek nopeusrajoitusasetuksissa. Oltava positiivinen numero" + }, + { + "id": "model.config.is_valid.rate_mem.app_error", + "translation": "Virheellinen muistivaraston koko nopeusrajoitusasetuksissa. Oltava positiivinen numero" + }, + { + "id": "model.config.is_valid.password_length.app_error", + "translation": "Salasanan minimipituuden on oltava kokonaisluku, vähintään {{.MinLength}} ja enintään {{.MaxLength}}." + }, + { + "id": "model.config.is_valid.message_export.global_relay.smtp_username.app_error", + "translation": "Viestin vientityön GlobalRelaySettings.SmtpUsername on asetettava" + }, + { + "id": "model.config.is_valid.message_export.global_relay.smtp_password.app_error", + "translation": "Viestin vientityön GlobalRelaySettings.SmtpPassword on asetettava" + }, + { + "id": "model.config.is_valid.message_export.global_relay.email_address.app_error", + "translation": "Viestin vientityön GlobalRelaySettings.EmailAddress oltava sähköpostiosoite" + }, + { + "id": "model.config.is_valid.message_export.global_relay.customer_type.app_error", + "translation": "Viestin vientityön GlobalRelaySettings.CustomerType oltava joko 'A9' tai 'A10'" + }, + { + "id": "model.config.is_valid.message_export.global_relay.config_missing.app_error", + "translation": "Viestin vientityön ExportFormat on 'globalrelay', mutta GlobalRelaySettings puuttuu" + }, + { + "id": "model.config.is_valid.message_export.export_type.app_error", + "translation": "Viestin vientityön ExportFormat oltava 'actiance', 'csv' tai 'globalrelay'" + }, + { + "id": "model.config.is_valid.message_export.export_from.app_error", + "translation": "Viestin vientityön ExportFromTimestamp oltava aika (sekunteja unix epochin jälkeen). Vain tämän ajan jälkeen lähetetyt viestit viedään." + }, + { + "id": "model.config.is_valid.message_export.enable.app_error", + "translation": "Viestin vientityön EnableExport asetus oltava joko true tai false" + }, + { + "id": "model.config.is_valid.message_export.daily_runtime.app_error", + "translation": "Viestin vientityön DailyRuntime oltava 24-tunnin aika muodossa HH:MM." + }, + { + "id": "model.config.is_valid.message_export.batch_size.app_error", + "translation": "Viestin vientityön BatchSize oltava positiivinen kokonaisluku" + }, + { + "id": "model.config.is_valid.max_users.app_error", + "translation": "Virheellinen käyttäjien maksiminäärä per tiimi tiimiasetuksissa. Oltava positiivinen numero." + }, + { + "id": "model.config.is_valid.max_notify_per_channel.app_error", + "translation": "Virheellinen ilmoitusraja per kanava tiimiasetuksissa. Oltava positiivinen numero." + }, + { + "id": "model.config.is_valid.max_file_size.app_error", + "translation": "Virheellinen maksimitiedostokoko tiedostoasetuksissa. Oltava nollaa suurempi kokonaisluku." + }, + { + "id": "model.config.is_valid.max_channels.app_error", + "translation": "Virheellinen kanavaraja per tiimi tiimiasetuksissa. Oltava positiivinen numero." + }, + { + "id": "model.config.is_valid.max_burst.app_error", + "translation": "Maksimipurskekoon oltava suurempi kuin nolla." + }, + { + "id": "model.config.is_valid.login_attempts.app_error", + "translation": "Virheellinen kirjatumisyritysten maksimimäärä palveluasetuksissa. Oltava positiivinen numero." + }, + { + "id": "model.config.is_valid.localization.available_locales.app_error", + "translation": "Tarjottavien kielien on sisällytettävä oletusasiakaskieli" + }, + { + "id": "model.config.is_valid.listen_address.app_error", + "translation": "Virheellinen kuunteluosoite palveluasetuksissa Oltava asetettu." + }, + { + "id": "model.config.is_valid.ldap_username", + "translation": "AD/LDAP kenttä \"Username Attribute\" on pakollinen." + }, + { + "id": "model.config.is_valid.ldap_sync_interval.app_error", + "translation": "Virheellinen synkronointiintervalli. Oltava vähintään yksi minuutti." + }, + { + "id": "model.config.is_valid.ldap_server", + "translation": "AD/LDAP kenttä \"AD/LDAP Server\" on pakollinen." + }, + { + "id": "model.config.is_valid.ldap_security.app_error", + "translation": "Virheellinen yhteysturva AD/LDAP asetuksissa. Oltava '', 'TLS' tai 'STARTTLS'" + }, + { + "id": "model.config.is_valid.ldap_max_page_size.app_error", + "translation": "Virheellinen sivun maksimikoko." + }, + { + "id": "model.config.is_valid.ldap_login_id", + "translation": "AD/LDAP kenttä \"Login ID Attribute\" on pakollinen." + }, + { + "id": "model.config.is_valid.ldap_id", + "translation": "AD/LDAP kenttä \"ID Attribute\" on pakollinen." + }, + { + "id": "model.config.is_valid.ldap_email", + "translation": "AD/LDAP kenttä \"Email Attribute\" on pakollinen." + }, + { + "id": "model.config.is_valid.ldap_basedn", + "translation": "AD/LDAP kenttä \"BaseDN\" on pakollinen." + }, + { + "id": "model.config.is_valid.image_proxy_type.app_error", + "translation": "Virheellinen kuvavälityspalvelutyyppi. Oltava 'local' tai 'atmos/camo'." + }, + { + "id": "model.config.is_valid.group_unread_channels.app_error", + "translation": "Virheellinen lukemattomien kanavien ryhmä palveluasetuksissa. Oltava 'disabled', 'default_on' tai 'default_off'." + }, + { + "id": "model.config.is_valid.file_salt.app_error", + "translation": "Virheellinen julkisen linkin suola tiedostoasetuksissa. Oltava vähintään 32 merkkiä." + }, + { + "id": "model.config.is_valid.file_driver.app_error", + "translation": "Virheellinen ajurinimi tiedostoasetuksille. On oltava 'local' tai 'amazons3'" + }, + { + "id": "model.config.is_valid.encrypt_sql.app_error", + "translation": "Virheellinen lepotilan salausavain SQL asetuksissa. Oltava vähintään 32 merkkiä." + }, + { + "id": "model.config.is_valid.email_security.app_error", + "translation": "Virheellinen yhteyden turvallisuus sähköpostiasetuksissa. Oltava '', 'TLS' tai 'STARTTLS'" + }, + { + "id": "model.config.is_valid.email_notification_contents_type.app_error", + "translation": "Virheellinen sähköposti-ilmoituksen sisältötyyppi sähköpostiasetuksissa. Oltava joko 'full' tai 'generic'." + }, + { + "id": "model.config.is_valid.email_batching_interval.app_error", + "translation": "Virheellinen sähköpostin yhdistelyn intervalli sähköpostiosoitteissa. Oltava vähintään 30 sekuntia." + }, + { + "id": "model.config.is_valid.email_batching_buffer_size.app_error", + "translation": "Virheellinen sähköpostien yhdistelyn puskurikoko sähköpostiasetuksissa. Arvon oltava 0 tai positiivinen numero." + }, + { + "id": "model.config.is_valid.elastic_search.request_timeout_seconds.app_error", + "translation": "Elasticsearch Request Timeout oltava vähintään 1 sekunti." + }, + { + "id": "model.config.is_valid.elastic_search.posts_aggregator_job_start_time.app_error", + "translation": "Elasticsearch PostsAggregatorJobStartTime arvon on oltava aika muodossa \"hh:mm\"" + }, + { + "id": "model.config.is_valid.elastic_search.live_indexing_batch_size.app_error", + "translation": "Elasticsearch Live Indexing Batch Size oltava vähintään 1" + }, + { + "id": "model.config.is_valid.elastic_search.enable_searching.app_error", + "translation": "Elasticsearch IndexingEnabled asetus on asetettava päälle kun Elasticsearch SearchEnabled asetetaan päälle." + }, + { + "id": "model.config.is_valid.elastic_search.enable_autocomplete.app_error", + "translation": "Elasticsearch IndexingEnabled asetus on asetettava päälle kun Elasticsearch SearchEnabled asetetaan päälle." + }, + { + "id": "model.config.is_valid.elastic_search.connection_url.app_error", + "translation": "Elasticsearch ConnectionUrl asetus on annettava kun Elasticsearch indeksointi kytketään päälle." + }, + { + "id": "model.config.is_valid.elastic_search.aggregate_posts_after_days.app_error", + "translation": "Elasticsearch AggregatePostsAfterDays asetuksen on oltava vähintään 1" + }, + { + "id": "model.config.is_valid.display.custom_url_schemes.app_error", + "translation": "Oma URLin skeema {{.Scheme}} on virheellinen. Omien URL skeemojen on alettava kirjaimella ja muodostuva vain kirjaimiista, numeroista, plus (+), piste (.) ja viiva (-)." + }, + { + "id": "model.config.is_valid.data_retention.message_retention_days_too_low.app_error", + "translation": "Viestin säilyttäminen oltava vähintään 1 päivä." + }, + { + "id": "model.config.is_valid.data_retention.file_retention_days_too_low.app_error", + "translation": "Tiedoston säilyttäminen on oltava vähintään 1 päivä." + }, + { + "id": "model.config.is_valid.data_retention.deletion_job_start_time.app_error", + "translation": "Tietojensäilyttämistyön aloitusaika on 24-tunnin aikaleima muodossa HH:MM." + }, + { + "id": "model.config.is_valid.cluster_email_batching.app_error", + "translation": "Sähköpostien yhdistelyä ei voi ottaa käyttöön kun klusterointi on käytössä." + }, + { + "id": "model.config.is_valid.bleve_search.enable_searching.app_error", + "translation": "Elasticsearch IndexingEnabled asetus on asetettava päälle kun Elasticsearch SearchEnabled asetetaan päälle." + }, + { + "id": "model.config.is_valid.bleve_search.enable_autocomplete.app_error", + "translation": "Elasticsearch IndexingEnabled asetus on asetettava päälle kun Elasticsearch SearchEnabled asetetaan päälle." + }, + { + "id": "model.config.is_valid.atmos_camo_image_proxy_url.app_error", + "translation": "Virheellinen RemoteImageProxyURL arvo atmos/camoa varten. Täytyy asettaa jaettuun avaimeen." + }, + { + "id": "model.config.is_valid.atmos_camo_image_proxy_options.app_error", + "translation": "Virheellinen RemoteImageProxyOptions arvo atmos/camoa varten. Täytyy asettaa jaettuun avaimeen." + }, + { + "id": "model.config.is_valid.allow_cookies_for_subdomains.app_error", + "translation": "Evästeiden salliminen alidomaineihin vaatii SiteURL arvon asettamista." + }, + { + "id": "model.compliance.is_valid.start_end_at.app_error", + "translation": "To arvon oltava suurempi kuin From" + }, + { + "id": "model.compliance.is_valid.start_at.app_error", + "translation": "From on oltava aika" + }, + { + "id": "model.compliance.is_valid.id.app_error", + "translation": "Virheellinen ID" + }, + { + "id": "model.compliance.is_valid.end_at.app_error", + "translation": "To on oltava aika" + }, + { + "id": "model.compliance.is_valid.desc.app_error", + "translation": "Viallinen kuvaus" + }, + { + "id": "model.compliance.is_valid.create_at.app_error", + "translation": "Create at arvon oltava validi aika" + }, + { + "id": "model.command_hook.user_id.app_error", + "translation": "Virheellinen käyttäjä-id" + }, + { + "id": "model.command_hook.root_id.app_error", + "translation": "Virheellinen juuren ID" + }, + { + "id": "model.command_hook.id.app_error", + "translation": "Virheellinen komennon hook-ID" + }, + { + "id": "model.command_hook.create_at.app_error", + "translation": "Create at arvon oltava validi aika" + }, + { + "id": "model.command_hook.command_id.app_error", + "translation": "Virheellinen komennon ID" + }, + { + "id": "model.command_hook.channel_id.app_error", + "translation": "Virheellinen kanavan id" + }, + { + "id": "model.command.is_valid.user_id.app_error", + "translation": "Virheellinen käyttäjä-id" + }, + { + "id": "model.command.is_valid.url_http.app_error", + "translation": "Viallinen URL. URLin on oltava kunnollinen URL ja alettava http:// tai https://" + }, + { + "id": "model.command.is_valid.url.app_error", + "translation": "Virheellinen URL" + }, + { + "id": "model.command.is_valid.update_at.app_error", + "translation": "Update at on oltava aika" + }, + { + "id": "model.command.is_valid.trigger.app_error", + "translation": "Virheellinen trigger" + }, + { + "id": "model.command.is_valid.token.app_error", + "translation": "Virheellinen token." + }, + { + "id": "model.command.is_valid.team_id.app_error", + "translation": "Virheellinen tiimin ID" + }, + { + "id": "store.sql_user.get_for_login.app_error", + "translation": "Ei löydetty olemassaolevaa tiliä jolla tunnuksesi. Tiimi voi vaatia kutsun tiiminomistajalta ennen liittymistä." + }, + { + "id": "model.command.is_valid.method.app_error", + "translation": "Virheellinen metodi" + }, + { + "id": "model.command.is_valid.id.app_error", + "translation": "Virheellinen ID" + }, + { + "id": "model.command.is_valid.display_name.app_error", + "translation": "Virheellinen otsikko" + }, + { + "id": "model.command.is_valid.description.app_error", + "translation": "Viallinen kuvaus" + }, + { + "id": "model.command.is_valid.create_at.app_error", + "translation": "Create at arvon oltava validi aika" + }, + { + "id": "model.cluster.is_valid.type.app_error", + "translation": "Type on asetettava" + }, + { + "id": "model.cluster.is_valid.name.app_error", + "translation": "ClusterName on asetettava" + }, + { + "id": "model.cluster.is_valid.last_ping_at.app_error", + "translation": "LastPingAt on asetettava" + }, + { + "id": "model.cluster.is_valid.id.app_error", + "translation": "Virheellinen ID" + }, + { + "id": "model.cluster.is_valid.hostname.app_error", + "translation": "Hostname on asetettava" + }, + { + "id": "model.cluster.is_valid.create_at.app_error", + "translation": "CreateAt on asetettava" + }, + { + "id": "model.channel_member.is_valid.user_id.app_error", + "translation": "Virheellinen käyttäjä-id" + }, + { + "id": "model.channel_member.is_valid.unread_level.app_error", + "translation": "Virheellinen merkitse-lukemattomaksi-taso" + }, + { + "id": "model.channel_member.is_valid.push_level.app_error", + "translation": "Virheellinen push-viestitaso" + }, + { + "id": "model.channel_member.is_valid.notify_level.app_error", + "translation": "Virheellinen ilmoitustaso" + }, + { + "id": "model.channel_member.is_valid.ignore_channel_mentions_value.app_error", + "translation": "Virheellinen kanavan mainintojen ohitustila" + }, + { + "id": "model.channel_member.is_valid.email_value.app_error", + "translation": "Virheellinen sähköposti-ilmoituksen arvo" + }, + { + "id": "model.channel_member.is_valid.channel_id.app_error", + "translation": "Virheellinen kanavan id" + }, + { + "id": "model.channel.is_valid.update_at.app_error", + "translation": "Update at on oltava aika" + }, + { + "id": "model.channel.is_valid.type.app_error", + "translation": "Virheellinen type" + }, + { + "id": "model.channel.is_valid.purpose.app_error", + "translation": "Virheellinen tarkoitus" + }, + { + "id": "model.channel.is_valid.id.app_error", + "translation": "Virheellinen ID" + }, + { + "id": "model.channel.is_valid.header.app_error", + "translation": "Virheellinen otsikko" + }, + { + "id": "model.channel.is_valid.display_name.app_error", + "translation": "Viallinen näyttönimi" + }, + { + "id": "model.channel.is_valid.creator_id.app_error", + "translation": "Virheellinen creator-ID" + }, + { + "id": "model.channel.is_valid.create_at.app_error", + "translation": "Create at arvon oltava validi aika" + }, + { + "id": "model.bot.is_valid.username.app_error", + "translation": "Virheellinen käyttäjänimi." + }, + { + "id": "model.bot.is_valid.user_id.app_error", + "translation": "Virheellinen käyttäjä-id" + }, + { + "id": "model.bot.is_valid.update_at.app_error", + "translation": "Virheellinen creator-ID" + }, + { + "id": "model.bot.is_valid.description.app_error", + "translation": "Viallinen kuvaus" + }, + { + "id": "model.bot.is_valid.creator_id.app_error", + "translation": "Virheellinen creator-ID" + }, + { + "id": "model.bot.is_valid.create_at.app_error", + "translation": "Virheellinen creator-ID" + }, + { + "id": "model.authorize.is_valid.user_id.app_error", + "translation": "Virheellinen käyttäjä-id" + }, + { + "id": "model.authorize.is_valid.state.app_error", + "translation": "Virheellinen state" + }, + { + "id": "model.authorize.is_valid.scope.app_error", + "translation": "Virheellinen scope" + }, + { + "id": "model.authorize.is_valid.response_type.app_error", + "translation": "Virheellinen vastaustyyppi" + }, + { + "id": "model.authorize.is_valid.redirect_uri.app_error", + "translation": "Virheellinen uudelleenohjaus-URI" + }, + { + "id": "model.authorize.is_valid.expires.app_error", + "translation": "Expires in on asetettava" + }, + { + "id": "model.authorize.is_valid.create_at.app_error", + "translation": "Create at arvon oltava validi aika" + }, + { + "id": "model.authorize.is_valid.client_id.app_error", + "translation": "Virheellinen asiakas-ID" + }, + { + "id": "model.authorize.is_valid.auth_code.app_error", + "translation": "Virheellinen auktorisointikoodi" + }, + { + "id": "model.access.is_valid.user_id.app_error", + "translation": "Virheellinen käyttäjä-id" + }, + { + "id": "model.access.is_valid.refresh_token.app_error", + "translation": "Virheellinen päivitys-token" + }, + { + "id": "model.access.is_valid.redirect_uri.app_error", + "translation": "Virheellinen uudelleenohjaus-URI" + }, + { + "id": "model.access.is_valid.client_id.app_error", + "translation": "Virheellinen asiakas-ID" + }, + { + "id": "model.access.is_valid.access_token.app_error", + "translation": "Virheellinen access token" + }, + { + "id": "migrations.worker.run_migration.unknown_key", + "translation": "Ei voitu suorittaa migraatiotyötä tuntemattomasta migraatioavaimesta johtuen." + }, + { + "id": "migrations.worker.run_advanced_permissions_phase_2_migration.invalid_progress", + "translation": "Migraatio epäonnistui virheellisen editysmisdatan vuoksi." + }, + { + "id": "mfa.validate_token.authenticate.app_error", + "translation": "Virheellinen MFA token." + }, + { + "id": "mfa.mfa_disabled.app_error", + "translation": "Monivaiheinen tunnistus on kytketty pois tällä palvelimella." + }, + { + "id": "mfa.generate_qr_code.create_code.app_error", + "translation": "Virhe generoitaessa QR-koodia" + }, + { + "id": "mfa.activate.bad_token.app_error", + "translation": "Virheellinen MFA token." + }, + { + "id": "mattermost.bulletin.subject", + "translation": "Mattermost Turvallisuusilmoitus" + }, + { + "id": "manaultesting.test_autolink.unable.app_error", + "translation": "Ei voitu hakea kanavia" + }, + { + "id": "manaultesting.manual_test.parse.app_error", + "translation": "Ei voitu parsia URL arvoa" + }, + { + "id": "jobs.set_job_error.update.error", + "translation": "Ei voitu asettaa työtä tilaan virhe" + }, + { + "id": "jobs.request_cancellation.status.error", + "translation": "Ei voitu pyytää työn peruutusta koska työ ei ole peruutettavassa tilassa." + }, + { + "id": "interactive_message.generate_trigger_id.signing_failed", + "translation": "Ei voitu allekirjoittaa interaktiivisen dialogin liipaus IDtä." + }, + { + "id": "interactive_message.decode_trigger_id.verify_signature_failed", + "translation": "Interaktiivisen dialogin liipaisu-ID:n allekirjoituksen vahvistaminen epäonnistui." + }, + { + "id": "interactive_message.decode_trigger_id.signature_decode_failed", + "translation": "Ei voitu dekoodata interaktiivisen dialogin liipaisu-IDn base64 allekirjoitusta." + }, + { + "id": "interactive_message.decode_trigger_id.missing_data", + "translation": "Liipaisu-ID:stä puuttuu interaktiivista dialogia varten data." + }, + { + "id": "interactive_message.decode_trigger_id.expired", + "translation": "Interaktiivisen dialogin liipaisu-ID on vanhentunut. Liipaisu-IDt kestävät enintään {{.Seconds}} sekuntia." + }, + { + "id": "interactive_message.decode_trigger_id.base64_decode_failed_signature", + "translation": "Ei voitu dekoodata interaktiivisen dialogin liipaisu-IDn base64 allekirjoitusta." + }, + { + "id": "interactive_message.decode_trigger_id.base64_decode_failed", + "translation": "Ei voitu dekoodata interaktiivisen dialogin liipaisu-IDn base64:ää." + }, + { + "id": "ent.saml.service_disable.app_error", + "translation": "SAML 2.0 ei ole konfiguroitu tai ei ole tuettu tällä palvelimella." + }, + { + "id": "ent.saml.metadata.app_error", + "translation": "Palveluntarjoajan metadataa rakennettaessa tapahtui virhe." + }, + { + "id": "ent.saml.license_disable.app_error", + "translation": "Lisenssisi ei tue SAML-autentikointia." + }, + { + "id": "ent.saml.do_login.validate.app_error", + "translation": "Tapahtui virhe tarkistettaessa Identity Providerin vastausta. Ota yhteys järjestelmänvalvojaan." + }, + { + "id": "ent.saml.do_login.parse.app_error", + "translation": "Tapahtui virhe parsittaessa Identity Providerin vastausta. Ota yhteys järjestelmänvalvojaan." + }, + { + "id": "ent.saml.do_login.empty_response.app_error", + "translation": "Vastaanotettuun tyhjä vastaus Identity Providerilta" + }, + { + "id": "ent.saml.do_login.decrypt.app_error", + "translation": "SAML kirjautuminen epäonnistui koska tapahtui virhe purettaessa Identity Providerin vastausta. Ota yhteys järjestelmänvalvojaan." + }, + { + "id": "ent.saml.configure.not_encrypted_response.app_error", + "translation": "SAML kirjautuminen epäonnistui koska Identity Provider vastausta ei ollut salattu. Ota yhteys järjestelmänvalvojaan." + }, + { + "id": "ent.saml.configure.load_private_key.app_error", + "translation": "SAML kirjautuminen epäonnistui koska Service Provider Private Key ei löydetty. Ota yhteys järjestelmänvalvojaan." + }, + { + "id": "ent.saml.configure.load_idp_cert.app_error", + "translation": "Identity Provider Public Certificate tiedostoa ei löydetty. Ota yhteys järjestelmänvalvojaan." + }, + { + "id": "ent.saml.configure.encryption_not_enabled.app_error", + "translation": "SAML kirjautuminen epäonnistui koska salaus ei ole käytössä. Ota yhteys järjestelmänvalvojaan." + }, + { + "id": "ent.saml.build_request.encoding.app_error", + "translation": "Tapahtui virhe enkoodattaessa pyyntöä Identity Provideriin. Ota yhteys järjestelmänvalvojaan." + }, + { + "id": "ent.saml.build_request.app_error", + "translation": "Tapahtui virhe aloitettaessa kutsua Identity Provideriin. Ota yhteys järjestelmänvalvojaan." + }, + { + "id": "ent.saml.attribute.app_error", + "translation": "SAML-kirjautuminen epäonnistui koska yksi attribuuteista oli virheellinen. Ota yhteys järjestelmänvalvojaan." + }, + { + "id": "ent.migration.migratetosaml.username_already_used_by_other_user", + "translation": "Käyttäjätunnus on jo toisen Mattermost-käyttäjän käytössä." + }, + { + "id": "ent.migration.migratetosaml.user_not_found_in_users_mapping_file", + "translation": "Käyttäjää ei löydetty users-tiedostosta." + }, + { + "id": "ent.migration.migratetosaml.email_already_used_by_other_user", + "translation": "Sähköposti on jo toisella SAML-käyttäjällä." + }, + { + "id": "ent.migration.migratetoldap.user_not_found", + "translation": "Ei löydetty käyttäjää AD/LDAP palvelusta: " + }, + { + "id": "ent.migration.migratetoldap.duplicate_field", + "translation": "Ei voitu migroida AD/LDAP käyttäjiä määritetyllä kentällä. Duplikaattisyöte havaittu. Poista kaikki duplikaatit ja yritä uudestaan." + }, + { + "id": "ent.message_export.global_relay_export.deliver.unable_to_open_zip_file_data.app_error", + "translation": "Ei voitu avata väliaikaista vientitiedostoa" + }, + { + "id": "ent.message_export.global_relay_export.deliver.unable_to_open_email_file.app_error", + "translation": "Ei voitu hakea sähköpostia väliaikaistiedostosta" + }, + { + "id": "ent.message_export.global_relay_export.deliver.unable_to_get_file_info.app_error", + "translation": "Ei voitu hakea tietoa väliaikaisesta vientitiedostosta" + }, + { + "id": "ent.message_export.global_relay_export.deliver.to_address.app_error", + "translation": "Ei voitu asettaa sähköpostin Vastaanottaja-osoitetta." + }, + { + "id": "ent.message_export.global_relay_export.deliver.parse_mail.app_error", + "translation": "Ei voitu lukea sähköpostitietoa." + }, + { + "id": "ent.message_export.global_relay_export.deliver.msg_data.app_error", + "translation": "Ei voitu kirjoittaa sähköpostin viestiä." + }, + { + "id": "ent.message_export.global_relay_export.deliver.msg.app_error", + "translation": "Ei voitu asettaa sähköpostin viestiä." + }, + { + "id": "ent.message_export.global_relay_export.deliver.from_address.app_error", + "translation": "Ei voitu asettaa sähköpostin Lähettäjä-osoitetta." + }, + { + "id": "ent.message_export.global_relay_export.deliver.close.app_error", + "translation": "Ei voitu lähettää sähköpostia Global Relaylle." + }, + { + "id": "ent.message_export.global_relay.generate_email.app_error", + "translation": "Ei voitu generoida eml tiedostodataa." + }, + { + "id": "ent.message_export.global_relay.create_file_in_zip.app_error", + "translation": "Ei voitu luoda eml tiedostoa." + }, + { + "id": "ent.message_export.global_relay.close_zip_file.app_error", + "translation": "Ei voitu sulkea zip-tiedostoa." + }, + { + "id": "ent.message_export.global_relay.attach_file.app_error", + "translation": "Ei voitu liittää liitettä Global Relay vientiin." + }, + { + "id": "ent.ldap_groups.reachable_groups_error", + "translation": "virhe haettaessa ryhmiä käyttäjälle" + }, + { + "id": "ent.ldap_groups.no_rows", + "translation": "ei löydetty ryhmiä joilla uid" + }, + { + "id": "ent.ldap_groups.members_of_group_error", + "translation": "virhe haettaessa ryhmän jäseniä" + }, + { + "id": "ent.ldap_groups.groups_search_error", + "translation": "virhe haettaessa ldap-ryhmiä" + }, + { + "id": "ent.ldap_groups.group_search_error", + "translation": "virhe haettaessa ldap-ryhmää" + }, + { + "id": "ent.ldap.validate_guest_filter.app_error", + "translation": "Virheellinen AD/LDAP suodatin" + }, + { + "id": "ent.ldap.validate_filter.app_error", + "translation": "Virheellinen AD/LDAP suodatin" + }, + { + "id": "ent.ldap.validate_admin_filter.app_error", + "translation": "Virheellinen AD/LDAP suodatin" + }, + { + "id": "ent.ldap.syncronize.search_failure.app_error", + "translation": "Ei voitu etsiä käyttäjiä AD/LDAPista. Tarkista että Mattermost palvelu voi yhdistää AD/LDAP palveluun ja yritä uudestaan." + }, + { + "id": "ent.ldap.syncronize.populate_syncables", + "translation": "virhe populoitaessa synkronoitavia" + }, + { + "id": "ent.ldap.syncronize.get_all_groups.app_error", + "translation": "virhe haettaessa ryhmiä" + }, + { + "id": "ent.ldap.syncronize.get_all.app_error", + "translation": "Ei voitu hakea kaikkia käyttäjiä AD/LDAP:ia käyttäen" + }, + { + "id": "ent.ldap.syncronize.delete_group_constained_memberships", + "translation": "virhe poistettaessa tiimiä tai kanavan jäsenyyksiä" + }, + { + "id": "ent.ldap.do_login.user_not_registered.app_error", + "translation": "Käyttäjä ei ole rekisteröity AD/LDAP palveluun" + }, + { + "id": "ent.ldap.do_login.user_filtered.app_error", + "translation": "AD/LDAP tililläsi ei ole lupaa käyttää tätä Mattermost palvelua. Pyydä järjestelmänvalvojaa tarkistamaan AD/LDAP käyttäjäsuodatuksen." + }, + { + "id": "ent.ldap.do_login.unable_to_connect.app_error", + "translation": "Ei voitu yhdistää AD/LDAP palveluun" + }, + { + "id": "ent.ldap.do_login.search_ldap_server.app_error", + "translation": "Ei voitu etsiä AD/LDAP palvelusta" + }, + { + "id": "ent.ldap.do_login.matched_to_many_users.app_error", + "translation": "Annettu käyttäjätunnus vastaa useita käyttäjiä" + }, + { + "id": "ent.ldap.do_login.licence_disable.app_error", + "translation": "Nykyinen lisenssi ei tue AD/LDAP toiminnallisuutta. Kysy järjestelmänvalvojalta enterprise-lisenssiin vaihtamisesta." + }, + { + "id": "ent.ldap.do_login.invalid_password.app_error", + "translation": "Virheellinen salasana" + }, + { + "id": "ent.ldap.do_login.bind_admin_user.app_error", + "translation": "Ei voitu bindata AD/LDAP palveluun. Tarkista BindUsername ja BindPassword." + }, + { + "id": "ent.ldap.disabled.app_error", + "translation": "AD/LDAP kytketty pois tai lisenssi ei tue sitä." + }, + { + "id": "ent.ldap.create_fail", + "translation": "Ei voitu luoda LDAP käyttäjää." + }, + { + "id": "app.import.validate_user_teams_import_data.team_name_missing.error", + "translation": "Tiimin nimi puuttuu käyttäjän tiimijäsenyydestä." + }, + { + "id": "app.import.validate_user_teams_import_data.invalid_roles.error", + "translation": "Virheelliset roolit käyttäjän tiimijäsenyydessä." + }, + { + "id": "app.import.validate_user_import_data.username_missing.error", + "translation": "Puuttuva edellytetty käyttäjän ominaisuus: käyttäjätunnus." + }, + { + "id": "app.import.validate_user_import_data.username_invalid.error", + "translation": "Käyttäjätunnus on virheellinen." + }, + { + "id": "app.import.validate_user_import_data.roles_invalid.error", + "translation": "Käyttäjän roolit ovat virheelliset." + }, + { + "id": "app.import.validate_user_import_data.profile_image.error", + "translation": "Virheellinen profiilikuva." + }, + { + "id": "app.import.validate_user_import_data.position_length.error", + "translation": "Käyttäjän Position on liian pitkä." + }, + { + "id": "app.import.validate_user_import_data.password_length.error", + "translation": "Käyttäjän salasanan pituus on virheellinen." + }, + { + "id": "app.import.validate_user_import_data.notify_props_mobile_push_status_invalid.error", + "translation": "Virheellinen Mobile Push Status Notify Prop käyttäjälle." + }, + { + "id": "app.import.validate_user_import_data.notify_props_mobile_invalid.error", + "translation": "Virheellinen Mobile Notify Prop arvo käyttäjälle." + }, + { + "id": "app.import.validate_user_import_data.notify_props_email_invalid.error", + "translation": "Virheellinen Email Notify Prop arvo käyttäjälle." + }, + { + "id": "app.import.validate_user_import_data.notify_props_desktop_sound_invalid.error", + "translation": "Virheellinen Desktop Sound Notify Prop arvo käyttäjälle." + }, + { + "id": "app.import.validate_user_import_data.notify_props_desktop_invalid.error", + "translation": "Virheellinen Desktop Notify Prop arvo käyttäjälle." + }, + { + "id": "app.import.validate_user_import_data.notify_props_comments_trigger_invalid.error", + "translation": "Virheellinen Comments Prop arvo käyttäjälle." + }, + { + "id": "app.import.validate_user_import_data.notify_props_channel_trigger_invalid.error", + "translation": "Virheellinen Channel Trigger Notify Prop käyttäjälle." + }, + { + "id": "app.import.validate_user_import_data.nickname_length.error", + "translation": "Käyttäjän kutsumanimi on liian pitkä." + }, + { + "id": "app.import.validate_user_import_data.last_name_length.error", + "translation": "Käyttäjän Sukunimi on liian pitkä." + }, + { + "id": "app.import.validate_user_import_data.first_name_length.error", + "translation": "Käyttäjän Etunimi on liian pitkä." + }, + { + "id": "app.import.validate_user_import_data.email_missing.error", + "translation": "Puuttuva edellytetty käyttäjän ominaisuus: sähköposti." + }, + { + "id": "app.import.validate_user_import_data.email_length.error", + "translation": "Käyttäjän sähköpostin pituus on virheellinen." + }, + { + "id": "app.import.validate_user_import_data.auth_data_length.error", + "translation": "Käyttäjän AuthData on liian pitkä." + }, + { + "id": "app.import.validate_user_import_data.auth_data_and_password.error", + "translation": "Käyttäjän AuthData ja Password ovat toisensa pois sulkevia." + }, + { + "id": "app.import.validate_user_channels_import_data.invalid_roles.error", + "translation": "Virheelliset roolit käyttäjän kanavajäsenyydessä." + }, + { + "id": "app.import.validate_user_channels_import_data.invalid_notify_props_mobile.error", + "translation": "Virheellinen Mobile NotifyProps käyttäjän kanavajäsenyydessä." + }, + { + "id": "app.import.validate_user_channels_import_data.invalid_notify_props_mark_unread.error", + "translation": "Virheellinen MarkUnread NotifyProps käyttäjän kanavajäsenyydessä." + }, + { + "id": "app.import.validate_user_channels_import_data.invalid_notify_props_desktop.error", + "translation": "Virheellinen Desktop NotifyProps käyttäjän kanavajäsenyydessä." + }, + { + "id": "app.import.validate_user_channels_import_data.channel_name_missing.error", + "translation": "Kanavan nimi puuttuu käyttäjän kanavajäsenyydestä." + }, + { + "id": "app.import.validate_team_import_data.type_missing.error", + "translation": "Puuttuva vaadittu tiimin ominaisuus: type." + }, + { + "id": "app.import.validate_team_import_data.type_invalid.error", + "translation": "Tiimin tyyppi on viallinen." + }, + { + "id": "app.import.validate_team_import_data.scheme_invalid.error", + "translation": "Virheellinen järjestelyn nimi tiimille." + }, + { + "id": "app.import.validate_team_import_data.name_reserved.error", + "translation": "Tiimin nimessä ei saa olla varattuja sanoja." + }, + { + "id": "app.import.validate_team_import_data.name_missing.error", + "translation": "Puuttuva vaadittu tiimin ominaisuus: name." + }, + { + "id": "app.import.validate_team_import_data.name_length.error", + "translation": "Tiimin nimi on liian pitkä." + }, + { + "id": "app.import.validate_team_import_data.name_characters.error", + "translation": "Tiimin nimessä on kiellettyjä merkkejä." + }, + { + "id": "app.import.validate_team_import_data.display_name_missing.error", + "translation": "Puuttuva vaadittu tiimin ominaisuus: display_name." + }, + { + "id": "app.import.validate_team_import_data.display_name_length.error", + "translation": "Tiimin display_name ei ole sallittujen pituusrajojen mukainen." + }, + { + "id": "app.import.validate_team_import_data.description_length.error", + "translation": "Tiimin kuvaus on liian pitkä." + }, + { + "id": "app.import.validate_scheme_import_data.wrong_roles_for_scope.error", + "translation": "Väärän roolit annettu järjestelylle jolla on tämä skooppi." + }, + { + "id": "app.import.validate_scheme_import_data.unknown_scheme.error", + "translation": "Tuntematon järjestelyn skooppi." + }, + { + "id": "app.import.validate_scheme_import_data.null_scope.error", + "translation": "Järjestelyn skooppi vaaditaan." + }, + { + "id": "app.import.validate_scheme_import_data.name_invalid.error", + "translation": "Virheellinen järjestelyn nimi." + }, + { + "id": "app.import.validate_scheme_import_data.display_name_invalid.error", + "translation": "Viallinen järjestelyn näyttönimi." + }, + { + "id": "app.import.validate_scheme_import_data.description_invalid.error", + "translation": "Viallinen järjestelykuvaus." + }, + { + "id": "app.import.validate_role_import_data.name_invalid.error", + "translation": "Virheellinen roolinimi." + }, + { + "id": "app.import.validate_role_import_data.invalid_permission.error", + "translation": "Roolilla viallinen oikeus." + }, + { + "id": "app.import.validate_role_import_data.display_name_invalid.error", + "translation": "Viallinen roolin näyttönimi." + }, + { + "id": "app.import.validate_role_import_data.description_invalid.error", + "translation": "Viallinen roolin kuvaus." + }, + { + "id": "app.import.validate_reply_import_data.user_missing.error", + "translation": "Puuttuva vaadittu vastauksen ominaisuus: User." + }, + { + "id": "app.import.validate_reply_import_data.message_missing.error", + "translation": "Puuttuva vaadittu vastauksen ominaisuus: Message." + }, + { + "id": "app.import.validate_reply_import_data.message_length.error", + "translation": "Vastauksenn Message arvo on sallittua pidempi." + }, + { + "id": "app.import.validate_reply_import_data.create_at_zero.error", + "translation": "Vastauksen CreateAt arvo ei saa olla nolla." + }, + { + "id": "app.import.validate_reply_import_data.create_at_missing.error", + "translation": "Puuttuva vaadittu vastauksen ominaisuus: create_at." + }, + { + "id": "app.import.validate_reaction_import_data.user_missing.error", + "translation": "Puuttuva vaadittu reaktion ominaisuus: User." + }, + { + "id": "app.import.validate_reaction_import_data.emoji_name_missing.error", + "translation": "Puuttuva vaadittu reaktion ominaisuus: EmojiName." + }, + { + "id": "app.import.validate_reaction_import_data.emoji_name_length.error", + "translation": "Reaktion EmojiName arvo sallittua pidempi." + }, + { + "id": "app.import.validate_reaction_import_data.create_at_zero.error", + "translation": "Reaktion CreateAt arvo ei saa olla nolla." + }, + { + "id": "app.import.validate_reaction_import_data.create_at_missing.error", + "translation": "Puuttuva vaadittu reaktion ominaisuus: create_at." + }, + { + "id": "app.import.validate_reaction_import_data.create_at_before_parent.error", + "translation": "Reaktion CreateAt arvon oltava suurempi kuin emoviestin CreateAt." + }, + { + "id": "app.import.validate_post_import_data.user_missing.error", + "translation": "Puuttuva edellytetty käyttäjän ominaisuus: User." + }, + { + "id": "app.import.validate_post_import_data.team_missing.error", + "translation": "Puuttuva vaadittu viestin ominaisuus: Team." + }, + { + "id": "app.import.validate_post_import_data.message_missing.error", + "translation": "Puuttuva vaadittu viestin ominaisuus: Message." + }, + { + "id": "app.import.validate_post_import_data.message_length.error", + "translation": "Viestin Message arvo on sallittua pidempi." + }, + { + "id": "app.import.validate_post_import_data.create_at_zero.error", + "translation": "Viestin CreateAt arvo ei saa olla nolla." + }, + { + "id": "app.import.validate_post_import_data.create_at_missing.error", + "translation": "Puuttuva vaadittu viestin ominaisuus: create_at." + }, + { + "id": "app.import.validate_post_import_data.channel_missing.error", + "translation": "Puuttuva vaadittu viestin ominaisuus: Channel." + }, + { + "id": "app.import.validate_emoji_import_data.name_missing.error", + "translation": "Tuotu emojin nimikenttä puuttuu tai on tyhjä." + }, + { + "id": "app.import.validate_emoji_import_data.image_missing.error", + "translation": "Tuotu emojin kuvakenttä puuttuu tai on tyhjä." + }, + { + "id": "app.import.validate_emoji_import_data.empty.error", + "translation": "Tuotu emoji-data on tyhjä." + }, + { + "id": "app.import.validate_direct_post_import_data.user_missing.error", + "translation": "Puuttuva vaadittu yksityisviestin ominaisuus: user" + }, + { + "id": "app.import.validate_direct_post_import_data.unknown_flagger.error", + "translation": "Suoraviestin voi liputtaa vain kanavan jäsen. \"{{.Username}}\" ei ole jäsen." + }, + { + "id": "app.import.validate_direct_post_import_data.message_missing.error", + "translation": "Puuttuva vaadittu yksityisviestin ominaisuus: message" + }, + { + "id": "app.import.validate_direct_post_import_data.message_length.error", + "translation": "Viesti on liian pitkä" + }, + { + "id": "app.import.validate_direct_post_import_data.create_at_zero.error", + "translation": "CreateAt arvon oltava suurempi kuin 0" + }, + { + "id": "app.import.validate_direct_post_import_data.create_at_missing.error", + "translation": "Puuttuva vaadittu yksityisviestin ominaisuus: create_at" + }, + { + "id": "app.channel.update.bad_id", + "translation": "Ei voitu päivittää kanavaa" + }, + { + "id": "store.sql_channel.save_direct_channel.not_direct.app_error", + "translation": "Yritettiin luoda ei-suorakanava SaveDirectChannel komennolla" + }, + { + "id": "store.sql_channel.save_channel.limit.app_error", + "translation": "Olet saavuttanut kanavien lukumäärärajan." + }, + { + "id": "store.sql_channel.save_channel.exists.app_error", + "translation": "Tiimillä on jo samanniminen kanava" + }, + { + "id": "store.sql_channel.save_channel.existing.app_error", + "translation": "Kutsuttava päivitystä olemassaolevalle kanavalle" + }, + { + "id": "store.sql_channel.save.direct_channel.app_error", + "translation": "Käytä SaveDirectChannel luodaksesi suorakanavan" + }, + { + "id": "store.sql_channel.save.archived_channel.app_error", + "translation": "Et voi muokata arkistoitua kanavaa" + }, + { + "id": "store.sql_bot.get.missing.app_error", + "translation": "Bottia ei ole olemassa" + }, + { + "id": "app.audit.save.saving.app_error", + "translation": "Kohdattiin virhe tallennettaessa auditia" + }, + { + "id": "app.audit.permanent_delete_by_user.app_error", + "translation": "Kohdattiin virhe poistettaessa auditeja" + }, + { + "id": "app.audit.get.finding.app_error", + "translation": "Kohdattiin virhe etsiessä auditeja" + }, + { + "id": "plugin_api.send_mail.missing_to", + "translation": "Puuttuva vastaanottajan osoite." + }, + { + "id": "plugin_api.send_mail.missing_subject", + "translation": "Puuttuva sähköpostin otsikko." + }, + { + "id": "plugin_api.send_mail.missing_htmlbody", + "translation": "Puuttuva HTML runko." + }, + { + "id": "plugin_api.get_file_link.no_post.app_error", + "translation": "Ei voitu hakea julkista linkkiä tiedostolle. Tiedoston on oltava liitteenä viestissä joka voidaan lukea." + }, + { + "id": "plugin_api.get_file_link.disabled.app_error", + "translation": "Julkiset linkit on poistettu käytöstä" + }, + { + "id": "plugin_api.bot_cant_create_bot", + "translation": "Bottikäyttäjä ei voi luoda toista bottikäyttäjää." + }, + { + "id": "plugin.api.update_user_status.bad_status", + "translation": "Ei voitu asettaa käyttäjän tilaa. Tuntematon käyttäjän tila." + }, + { + "id": "oauth.gitlab.tos.error", + "translation": "GitLabin palveluehdot ovat muuttuneet. Mene osoitteeseen gitlab.com ja hyväksy ne, ja yritä sitten kirjautua Mattermostiin uudestaan." + }, + { + "id": "model.websocket_client.connect_fail.app_error", + "translation": "Ei voitu yhdistää WebSocket-palveluun." + }, + { + "id": "model.utils.decode_json.app_error", + "translation": "ei voitu dekoodata" + }, + { + "id": "model.user_access_token.is_valid.user_id.app_error", + "translation": "Virheellinen käyttäjä-id" + }, + { + "id": "model.user_access_token.is_valid.token.app_error", + "translation": "Virheellinen access token" + }, + { + "id": "model.user_access_token.is_valid.id.app_error", + "translation": "Virheellinen id arvo." + }, + { + "id": "model.user_access_token.is_valid.description.app_error", + "translation": "Virheellinen kuvaus, oltava enintään 255 merkkiä" + }, + { + "id": "model.user.is_valid.username.app_error", + "translation": "Käyttäjänimen on alettava kirjaimella, pituuden oltava 3-22 merkkiä jotka voivat olla numeroita, pieniä kirjaimia ja symboleita \".\", \"-\", ja \"_\"." + }, + { + "id": "model.user.is_valid.update_at.app_error", + "translation": "Update at on oltava aika" + }, + { + "id": "model.user.is_valid.pwd_uppercase_symbol.app_error", + "translation": "Salasanassasi oltava vähintään {{.Min}} merkkiä joista ainakin yksi iso kirjain ja yksi symboli (eli \"~!@#$%^&*()\")." + }, + { + "id": "model.user.is_valid.pwd_uppercase_number_symbol.app_error", + "translation": "Salasanassasi oltava vähintään {{.Min}} merkkiä joista ainakin yksi iso kirjain, yksi numero, ja yksi symboli (eli \"~!@#$%^&*()\")." + }, + { + "id": "model.user.is_valid.pwd_uppercase_number.app_error", + "translation": "Salasanassasi oltava vähintään {{.Min}} merkkiä joista ainakin yksi iso kirjain ja numero." + }, + { + "id": "model.user.is_valid.pwd_uppercase.app_error", + "translation": "Salasanassasi oltava vähintään {{.Min}} merkkiä joista ainakin yksi iso kirjain." + }, + { + "id": "model.user.is_valid.pwd_symbol.app_error", + "translation": "Salasanassasi oltava vähintään {{.Min}} merkkiä joista ainakin yksi symboli (eli \"~!@#$%^&*()\")." + }, + { + "id": "model.user.is_valid.pwd_number_symbol.app_error", + "translation": "Salasanassasi oltava vähintään {{.Min}} merkkiä joista ainakin yksi numero ja yksi symboli (eli \"~!@#$%^&*()\")." + }, + { + "id": "model.user.is_valid.pwd_number.app_error", + "translation": "Salasanassasi oltava vähintään {{.Min}} merkkiä joista ainakin yksi numero." + }, + { + "id": "model.user.is_valid.pwd_lowercase_uppercase_symbol.app_error", + "translation": "Salasanassasi oltava vähintään {{.Min}} merkkiä joista ainakin yksi pieni kirjain, yksi iso kirjain, ja yksi symboli (eli \"~!@#$%^&*()\")." + }, + { + "id": "model.user.is_valid.pwd_lowercase_uppercase_number_symbol.app_error", + "translation": "Salasanassasi oltava vähintään {{.Min}} merkkiä joista ainakin yksi pieni kirjain, yksi iso kirjain, yksi numero, ja yksi symboli (eli \"~!@#$%^&*()\")." + }, + { + "id": "model.user.is_valid.pwd_lowercase_uppercase_number.app_error", + "translation": "Salasanassasi oltava vähintään {{.Min}} merkkiä joista ainakin yksi pieni kirjain, yksi iso kirjain, ja yksi numero." + }, + { + "id": "model.user.is_valid.pwd_lowercase_uppercase.app_error", + "translation": "Salasanassasi oltava vähintään {{.Min}} merkkiä joista ainakin yksi pieni kirjain ja iso kirjain." + }, + { + "id": "model.user.is_valid.pwd_lowercase_symbol.app_error", + "translation": "Salasanassasi oltava vähintään {{.Min}} merkkiä joista ainakin yksi pieni kirjain ja yksi symboli (eli \"~!@#$%^&*()\")." + }, + { + "id": "model.user.is_valid.pwd_lowercase_number_symbol.app_error", + "translation": "Salasanassasi oltava vähintään {{.Min}} merkkiä joista ainakin yksi pieni kirjain, yksi numero, ja yksi symboli (eli \"~!@#$%^&*()\")." + }, + { + "id": "model.user.is_valid.pwd_lowercase_number.app_error", + "translation": "Salasanassasi oltava vähintään {{.Min}} merkkiä jossa ainakin yksi pieni kirjain ja yksi numero." + }, + { + "id": "model.user.is_valid.pwd_lowercase.app_error", + "translation": "Salasanassasi oltava vähintään {{.Min}} merkkiä joista ainakin yksi on pieni kirjain." + }, + { + "id": "model.user.is_valid.pwd.app_error", + "translation": "Salasanassasi oltava vähintään {{.Min}} merkkiä." + }, + { + "id": "model.user.is_valid.nickname.app_error", + "translation": "Virheelliset tiedostonimet" + }, + { + "id": "model.user.is_valid.locale.app_error", + "translation": "Virheelliset tiedostonimet" + }, + { + "id": "model.user.is_valid.last_name.app_error", + "translation": "Virheelliset tiedostonimet" + }, + { + "id": "model.user.is_valid.id.app_error", + "translation": "Virheellinen käyttäjä-id" + }, + { + "id": "model.user.is_valid.first_name.app_error", + "translation": "Virheelliset tiedostonimet" + }, + { + "id": "model.user.is_valid.email.app_error", + "translation": "Virheellinen sähköposti" + }, + { + "id": "model.user.is_valid.create_at.app_error", + "translation": "Create at arvon oltava validi aika" + }, + { + "id": "model.token.is_valid.size", + "translation": "Virheellinen token." + }, + { + "id": "model.token.is_valid.expiry", + "translation": "Virheellinen tokenin vanheneminen" + }, + { + "id": "model.team_member.is_valid.user_id.app_error", + "translation": "Virheellinen käyttäjä-id" + }, + { + "id": "model.team_member.is_valid.team_id.app_error", + "translation": "Virheellinen tiimin ID" + }, + { + "id": "model.team.is_valid.url.app_error", + "translation": "Virhellinen URLin tunnistaja" + }, + { + "id": "model.team.is_valid.update_at.app_error", + "translation": "Update at on oltava aika" + }, + { + "id": "model.team.is_valid.type.app_error", + "translation": "Virheellinen type" + }, + { + "id": "model.team.is_valid.reserved.app_error", + "translation": "URL ei ole käytettävissä. Yritä toista." + }, + { + "id": "model.team.is_valid.name.app_error", + "translation": "Virheellinen name" + }, + { + "id": "model.team.is_valid.invite_id.app_error", + "translation": "Virheellinen kutsun tunniste" + }, + { + "id": "model.team.is_valid.id.app_error", + "translation": "Virheellinen ID" + }, + { + "id": "model.team.is_valid.email.app_error", + "translation": "Virheellinen sähköposti" + }, + { + "id": "model.team.is_valid.domains.app_error", + "translation": "Virheellinen sallitut domainit" + }, + { + "id": "model.team.is_valid.description.app_error", + "translation": "Viallinen kuvaus" + }, + { + "id": "model.team.is_valid.create_at.app_error", + "translation": "Create at arvon oltava validi aika" + }, + { + "id": "model.team.is_valid.company.app_error", + "translation": "Virheellinen yrityksen nimi" + }, + { + "id": "model.team.is_valid.characters.app_error", + "translation": "Nimen on oltava vähintään 2 pientä kirjainta tai numeroa" + }, + { + "id": "model.reaction.is_valid.user_id.app_error", + "translation": "Virheellinen käyttäjä-id" + }, + { + "id": "model.reaction.is_valid.post_id.app_error", + "translation": "Virheellinen viestin id" + }, + { + "id": "model.reaction.is_valid.emoji_name.app_error", + "translation": "Virheellinen emojin nimi" + }, + { + "id": "model.reaction.is_valid.create_at.app_error", + "translation": "Create at arvon oltava validi aika" + }, + { + "id": "model.preference.is_valid.value.app_error", + "translation": "Arvo liian pitkä" + }, + { + "id": "model.preference.is_valid.theme.app_error", + "translation": "Virheellinen theme" + }, + { + "id": "model.preference.is_valid.name.app_error", + "translation": "Virheellinen name" + }, + { + "id": "model.preference.is_valid.id.app_error", + "translation": "Virheellinen käyttäjä-id" + }, + { + "id": "model.preference.is_valid.category.app_error", + "translation": "Virheellinen kategoria" + }, + { + "id": "model.post.is_valid.user_id.app_error", + "translation": "Virheellinen käyttäjä-id" + }, + { + "id": "model.post.is_valid.update_at.app_error", + "translation": "Update at on oltava aika" + }, + { + "id": "model.post.is_valid.type.app_error", + "translation": "Virheellinen type" + }, + { + "id": "model.post.is_valid.root_id.app_error", + "translation": "Virheellinen juuren ID" + }, + { + "id": "model.post.is_valid.props.app_error", + "translation": "Virheellinen props" + }, + { + "id": "model.post.is_valid.original_id.app_error", + "translation": "Virheellinen alkuperäinen ID" + }, + { + "id": "model.post.is_valid.msg.app_error", + "translation": "Virheellinen viesti" + }, + { + "id": "model.post.is_valid.id.app_error", + "translation": "Virheellinen ID" + }, + { + "id": "model.post.is_valid.hashtags.app_error", + "translation": "Virheelliset hashtagit" + }, + { + "id": "model.post.is_valid.filenames.app_error", + "translation": "Virheelliset tiedostonimet" + }, + { + "id": "model.post.is_valid.file_ids.app_error", + "translation": "Virheelliset tiedosto-IDt. Huomioi että tiedostolataukset on rajoittu max 5 kpl. Käytä useampia postauksia lisätiedostoja varten." + }, + { + "id": "model.post.is_valid.create_at.app_error", + "translation": "Create at arvon oltava validi aika" + }, + { + "id": "model.post.is_valid.channel_id.app_error", + "translation": "Virheellinen kanavan id" + }, + { + "id": "model.plugin_key_value.is_valid.plugin_id.app_error", + "translation": "Virheellinen lisäosan ID, oltava enemmän kuin {{.Min}} ja enintään {{.Max}} merkkiä pitkä." + }, + { + "id": "model.plugin_key_value.is_valid.key.app_error", + "translation": "Virheellinen avain, oltava enemmän kuin {{.Min}} ja enintään {{.Max}} merkkiä pitkä." + }, + { + "id": "model.plugin_command.error.app_error", + "translation": "Virhe tapahtui suoritettaessa komentoa." + }, + { + "id": "model.outgoing_hook.username.app_error", + "translation": "Virheellinen käyttäjänimi." + }, + { + "id": "model.outgoing_hook.is_valid.words.app_error", + "translation": "Virheelliset laukaisusanat" + }, + { + "id": "model.outgoing_hook.is_valid.user_id.app_error", + "translation": "Virheellinen käyttäjä-id" + }, + { + "id": "model.outgoing_hook.is_valid.url.app_error", + "translation": "Viallinen callback URL. Kunkin oltava kunnollinen URL ja alettava http:// tai https://" + }, + { + "id": "model.outgoing_hook.is_valid.update_at.app_error", + "translation": "Update at on oltava aika" + }, + { + "id": "model.outgoing_hook.is_valid.trigger_words.app_error", + "translation": "Virheelliset laukaisusanat" + }, + { + "id": "model.outgoing_hook.is_valid.token.app_error", + "translation": "Virheellinen token." + }, + { + "id": "model.outgoing_hook.is_valid.team_id.app_error", + "translation": "Virheellinen tiimin ID" + }, + { + "id": "model.outgoing_hook.is_valid.id.app_error", + "translation": "Virheellinen ID" + }, + { + "id": "model.outgoing_hook.is_valid.display_name.app_error", + "translation": "Virheellinen otsikko" + }, + { + "id": "model.outgoing_hook.is_valid.description.app_error", + "translation": "Viallinen kuvaus" + }, + { + "id": "model.outgoing_hook.is_valid.create_at.app_error", + "translation": "Create at arvon oltava validi aika" + }, + { + "id": "model.outgoing_hook.is_valid.content_type.app_error", + "translation": "Virheellinen content_type arvo" + }, + { + "id": "model.outgoing_hook.is_valid.channel_id.app_error", + "translation": "Virheellinen kanavan id" + }, + { + "id": "model.outgoing_hook.is_valid.callback.app_error", + "translation": "Virheelliset callback URLit" + }, + { + "id": "model.outgoing_hook.icon_url.app_error", + "translation": "Virheellinen ikoni" + }, + { + "id": "model.oauth.is_valid.update_at.app_error", + "translation": "Update at on oltava aika" + }, + { + "id": "model.oauth.is_valid.name.app_error", + "translation": "Virheellinen name" + }, + { + "id": "model.oauth.is_valid.icon_url.app_error", + "translation": "Ikonin URLin oltava kunnollinen URL ja alettava http:// tai https://." + }, + { + "id": "model.oauth.is_valid.homepage.app_error", + "translation": "Kotisivun oltava kunnollinen URL ja alettava http:// tai https://." + }, + { + "id": "model.oauth.is_valid.description.app_error", + "translation": "Viallinen kuvaus" + }, + { + "id": "model.oauth.is_valid.creator_id.app_error", + "translation": "Virheellinen creator-ID" + }, + { + "id": "model.oauth.is_valid.create_at.app_error", + "translation": "Create at arvon oltava validi aika" + }, + { + "id": "model.oauth.is_valid.client_secret.app_error", + "translation": "Virheellinen asiakassalaisuus" + }, + { + "id": "model.oauth.is_valid.callback.app_error", + "translation": "Callback URLin oltava kunnollinen URL ja alettava http:// tai https://." + }, + { + "id": "model.oauth.is_valid.app_id.app_error", + "translation": "Virhellinen apin id" + }, + { + "id": "model.link_metadata.is_valid.url.app_error", + "translation": "Linkin metadatan URL asetettava" + }, + { + "id": "model.link_metadata.is_valid.type.app_error", + "translation": "Virhellinen linkin metadatan tyyppi" + }, + { + "id": "model.link_metadata.is_valid.timestamp.app_error", + "translation": "Linkin metadatan aika oltava ei-nolla ja pyöristetty lähimpään tuntiin" + }, + { + "id": "model.link_metadata.is_valid.data_type.app_error", + "translation": "Linkin metadatan data ei täsmää annettuun tyyppiin" + }, + { + "id": "model.link_metadata.is_valid.data.app_error", + "translation": "Linkin metadatan data ei voi olla puuttuva" + }, + { + "id": "model.license_record.is_valid.id.app_error", + "translation": "Virheellinen id arvo ladattaessa lisenssiä." + }, + { + "id": "model.license_record.is_valid.create_at.app_error", + "translation": "Virheellinen create_at arvo lisenssiä ladattaessa." + }, + { + "id": "model.job.is_valid.type.app_error", + "translation": "Virheellinen job type" + }, + { + "id": "model.job.is_valid.status.app_error", + "translation": "Virheellinen työtila" + }, + { + "id": "model.job.is_valid.id.app_error", + "translation": "Virheellinen job id" + }, + { + "id": "model.job.is_valid.create_at.app_error", + "translation": "Create at arvon oltava validi aika" + }, + { + "id": "model.incoming_hook.username.app_error", + "translation": "Virheellinen käyttäjänimi." + }, + { + "id": "model.incoming_hook.user_id.app_error", + "translation": "Virheellinen käyttäjä-id" + }, + { + "id": "model.incoming_hook.update_at.app_error", + "translation": "Update at on oltava aika" + }, + { + "id": "model.incoming_hook.team_id.app_error", + "translation": "Virheellinen tiimin ID" + }, + { + "id": "model.incoming_hook.parse_data.app_error", + "translation": "Ei voitu lukea sisääntulevaa dataa" + }, + { + "id": "model.incoming_hook.id.app_error", + "translation": "Virheellinen ID" + }, + { + "id": "model.incoming_hook.icon_url.app_error", + "translation": "Virheellinen viestin ikoni" + }, + { + "id": "model.incoming_hook.display_name.app_error", + "translation": "Virheellinen otsikko" + }, + { + "id": "model.incoming_hook.description.app_error", + "translation": "Viallinen kuvaus" + }, + { + "id": "model.incoming_hook.create_at.app_error", + "translation": "Create at arvon oltava validi aika" + }, + { + "id": "model.incoming_hook.channel_id.app_error", + "translation": "Virheellinen kanavan id" + }, + { + "id": "model.guest.is_valid.emails.app_error", + "translation": "Virheellinen sähköposti" + }, + { + "id": "model.guest.is_valid.email.app_error", + "translation": "Virheellinen sähköposti" + }, + { + "id": "model.guest.is_valid.channels.app_error", + "translation": "Virheellinen kanavan id" + }, + { + "id": "model.guest.is_valid.channel.app_error", + "translation": "Virheellinen kanavan id" + }, + { + "id": "model.group_syncable.syncable_id.app_error", + "translation": "virheellinen synkronoitava id ryhmän synkronoitavalla" + }, + { + "id": "model.group_syncable.group_id.app_error", + "translation": "virheellinen ryhmä-id ryhmän synkronoitavalla" + }, + { + "id": "model.group_member.user_id.app_error", + "translation": "virheellinen käyttäjä-id ryhmän jäsenellä" + }, + { + "id": "model.group_member.group_id.app_error", + "translation": "virheellinen ryhmä-id ryhmän jäsenellä" + }, + { + "id": "model.group.update_at.app_error", + "translation": "virheellinen päivitys ryhmällä" + }, + { + "id": "model.group.source.app_error", + "translation": "virheellinen lähde ryhmällä" + }, + { + "id": "model.group.remote_id.app_error", + "translation": "virheellinen remote id ryhmällä" + }, + { + "id": "model.group.name.app_error", + "translation": "virheellinen nimi ryhmällä" + }, + { + "id": "model.group.display_name.app_error", + "translation": "virheellinen näyttönimi ryhmällä" + }, + { + "id": "model.group.description.app_error", + "translation": "virheellinen description arvo ryhmällä" + }, + { + "id": "model.group.create_at.app_error", + "translation": "virheellinen create at arvo ryhmällä" + }, + { + "id": "model.file_info.is_valid.user_id.app_error", + "translation": "Virheellinen user_id arvo." + }, + { + "id": "model.file_info.is_valid.update_at.app_error", + "translation": "Virheellinen update_at arvo." + }, + { + "id": "model.file_info.is_valid.post_id.app_error", + "translation": "Virheellinen post_id arvo." + }, + { + "id": "model.file_info.is_valid.path.app_error", + "translation": "Virheellinen path arvo." + }, + { + "id": "model.file_info.is_valid.id.app_error", + "translation": "Virheellinen id arvo." + }, + { + "id": "model.file_info.is_valid.create_at.app_error", + "translation": "Virheellinen create_at arvo." + }, + { + "id": "model.file_info.get.gif.app_error", + "translation": "Ei voitu dekoodata giffiä." + }, + { + "id": "model.emoji.user_id.app_error", + "translation": "Virheellinen creator-ID" + }, + { + "id": "model.emoji.update_at.app_error", + "translation": "Update at on oltava aika" + }, + { + "id": "model.emoji.name.app_error", + "translation": "Nimen oltava 1-64 pientä kirjainta tai numeroa" + }, + { + "id": "model.emoji.id.app_error", + "translation": "Virheellinen emojin id" + }, + { + "id": "model.emoji.create_at.app_error", + "translation": "Create at arvon oltava validi aika" + }, + { + "id": "model.config.is_valid.write_timeout.app_error", + "translation": "Virheellinen kirjoitusaikakatkaisun arvo." + }, + { + "id": "model.config.is_valid.websocket_url.app_error", + "translation": "Websocket URLin oltava kunnollinen URL ja alettava ws:// tai wss://" + }, + { + "id": "model.config.is_valid.webserver_security.app_error", + "translation": "Web-palvelun yhteysturvalla virheellinen arvo." + }, + { + "id": "model.config.is_valid.tls_overwrite_cipher.app_error", + "translation": "Virheellinen arvo TLS ylikirjoitus cipherille - Lue dokumentaatiosta sallitut arvot" + }, + { + "id": "model.config.is_valid.time_between_user_typing.app_error", + "translation": "Käyttäjän kirjoittamispäivitysten väli pitäisi olla vähintään 1000 millisekuntia." + }, + { + "id": "model.config.is_valid.teammate_name_display.app_error", + "translation": "Virheellinen tiimikaverin näyttö. Oltava 'full_name', 'nickname_full_name' tai 'username'" + }, + { + "id": "model.config.is_valid.sql_query_timeout.app_error", + "translation": "Virheellinen kyselyn aikakatkaisu SQL-asetuksissa. Oltava positiivinen numero." + }, + { + "id": "model.config.is_valid.sql_max_conn.app_error", + "translation": "Virheellinen maksimi avoin yhteys SQL-asetuksissa. Oltava positiivinen numero." + }, + { + "id": "model.config.is_valid.sql_idle.app_error", + "translation": "Virhellinen maksimi-jouto-yhteys SQL-asetuksissa. Oltava positiivinen numero." + }, + { + "id": "model.config.is_valid.sql_driver.app_error", + "translation": "Virheellinen ajurinimi SQL-asetuksille. On oltava 'mysql' tai 'postgres'" + }, + { + "id": "model.config.is_valid.sql_data_src.app_error", + "translation": "Virheellinen datalähde SQL-asetuksissa. Oltava asetettu." + }, + { + "id": "model.config.is_valid.sql_conn_max_lifetime_milliseconds.app_error", + "translation": "Virheellinen yhteyden maksimi-ikä SQL-asetuksissa. Oltava ei-negatiivinen numero." + }, + { + "id": "model.config.is_valid.sitename_length.app_error", + "translation": "Sivuston nimen oltava enintään {{.MaxLength}} merkkiä." + }, + { + "id": "model.config.is_valid.site_url_email_batching.app_error", + "translation": "Yhdistelyä ei voida ottaa käyttöön koska SiteURL ei ole asetettu." + }, + { + "id": "model.config.is_valid.site_url.app_error", + "translation": "Sivuston URLin oltava kunnollinen URL ja alettava http:// tai https://" + }, + { + "id": "model.config.is_valid.saml_username_attribute.app_error", + "translation": "Virheellinen Käyttäjätunnus attribuutti. Oltava asetettu." + }, + { + "id": "app.import.validate_direct_post_import_data.channel_members_too_many.error", + "translation": "Suoraviestikanavan jäsenlistassa liikaa kohtia" + }, + { + "id": "app.import.validate_direct_post_import_data.channel_members_too_few.error", + "translation": "Suoraviestikanavan jäsenlistassa ei tarpeeksi kohtia" + }, + { + "id": "app.import.validate_direct_post_import_data.channel_members_required.error", + "translation": "Puuttuva vaadittu suoraviestin ominaisuus: channel_members" + }, + { + "id": "app.import.validate_direct_channel_import_data.unknown_favoriter.error", + "translation": "Suorakanavan voi asettaa suosikiksi vain jäsen. \"{{.Username}}\" ei ole jäsen." + }, + { + "id": "app.import.validate_direct_channel_import_data.members_too_many.error", + "translation": "Suorakanavan jäsenlistassa liikaa kohtia" + }, + { + "id": "app.import.validate_direct_channel_import_data.members_too_few.error", + "translation": "Suorakanavan jäsenlistassa ei tarpeeksi kohtia" + }, + { + "id": "app.import.validate_direct_channel_import_data.members_required.error", + "translation": "Puuttuva vaadittu suorakanavan ominaisuus: members" + }, + { + "id": "app.import.validate_direct_channel_import_data.header_length.error", + "translation": "Suorakanavan otsikko on liian pitkä" + }, + { + "id": "app.import.validate_channel_import_data.type_missing.error", + "translation": "Puuttuva vaadittu kanavan ominaisuus: type." + }, + { + "id": "app.import.validate_channel_import_data.type_invalid.error", + "translation": "Kanavan tyyppi virheellinen." + }, + { + "id": "app.import.validate_channel_import_data.team_missing.error", + "translation": "Puuttuva vaadittu kanavan ominaisuus: team" + }, + { + "id": "app.import.validate_channel_import_data.scheme_invalid.error", + "translation": "Virheellinen järjestelyn nimi kanavalle." + }, + { + "id": "app.import.validate_channel_import_data.purpose_length.error", + "translation": "Kanavan tarkoitus on liian pitkä." + }, + { + "id": "app.import.validate_channel_import_data.name_missing.error", + "translation": "Puuttuva vaadittu kanavan ominaisuus: name" + }, + { + "id": "app.import.validate_channel_import_data.name_length.error", + "translation": "Kanavan nimi on liian pitkä." + }, + { + "id": "app.import.validate_channel_import_data.name_characters.error", + "translation": "Kanavan nimessä on kiellettyjä merkkejä." + }, + { + "id": "app.import.validate_channel_import_data.header_length.error", + "translation": "Kanavan otsikko on liian pitkä." + }, + { + "id": "app.import.validate_channel_import_data.display_name_length.error", + "translation": "Kanavan display_name ei ole sallittujen pituusrajojen mukainen." + }, + { + "id": "app.import.process_import_data_file_version_line.invalid_version.error", + "translation": "Ei voitu lukea versiota datan tuontitiedostosta." + }, + { + "id": "app.import.import_user_channels.save_preferences.error", + "translation": "Virhe tuotaessa käyttäjän kanavajäsenyyksiä. Asetuksien tallennus epäonnistui." + }, + { + "id": "app.import.import_user.save_preferences.error", + "translation": "Virhe tuotaessa käyttäjän asetuksia. Asetuksien tallennus epäonnistui." + }, + { + "id": "app.import.import_team.scheme_wrong_scope.error", + "translation": "Tiimi on asetettava team-skoopattuun järjestelyyn." + }, + { + "id": "app.import.import_team.scheme_deleted.error", + "translation": "Tiimiä ei voi asettaa käyttämään poistettua järjestelyä." + }, + { + "id": "app.import.import_scheme.scope_change.error", + "translation": "Bulkkituoja ei voi muuttaa jo olemassaolevan järjestelyn skooppia." + }, + { + "id": "app.import.import_post.user_not_found.error", + "translation": "Virhe tuotaessa viestiä. Käyttäjää jolla käyttäjänimi \"{{.Username}}\" ei löydetty." + }, + { + "id": "app.import.import_post.save_preferences.error", + "translation": "Virhe tuotaessa viestiä. Asetuksien tallennus epäonnistui." + }, + { + "id": "app.import.import_post.channel_not_found.error", + "translation": "Virhe tuotaessa viestiä. Kanavaa jolla nimi \"{{.ChannelName}}\" ei löydetty." + }, + { + "id": "app.import.import_line.unknown_line_type.error", + "translation": "Tuodussa datarivissä tuntematon tyyppi \"{{.Type}}\"." + }, + { + "id": "app.import.import_line.null_user.error", + "translation": "Tuodussa datarivissä tyyppinä \"user\" mutta user-objekti on null." + }, + { + "id": "app.import.import_line.null_team.error", + "translation": "Tuodussa datarivissä tyyppinä \"team\" mutta team-objekti on null." + }, + { + "id": "app.import.import_line.null_scheme.error", + "translation": "Tuodussa datarivissä tyyppinä \"scheme\" mutta scheme-objekti on null." + }, + { + "id": "app.import.import_line.null_post.error", + "translation": "Tuodussa datarivissä tyyppinä \"post\" mutta post-objekti on null." + }, + { + "id": "app.import.import_line.null_emoji.error", + "translation": "Tuodussa datarivissä tyyppinä \"emoji\" mutta emoji-objekti on null." + }, + { + "id": "app.import.import_line.null_direct_post.error", + "translation": "Tuodussa datarivissä tyyppinä \"direct_post\" mutta direct_post-objekti on null." + }, + { + "id": "app.import.import_line.null_direct_channel.error", + "translation": "Tuodussa datarivissä tyyppinä \"direct_channel\" mutta direct_channel-objekti on null." + }, + { + "id": "app.import.import_line.null_channel.error", + "translation": "Tuodussa datarivissä tyyppinä \"channel\" mutta channel-objekti on null." + }, + { + "id": "app.import.import_direct_post.create_group_channel.error", + "translation": "Ei voitu hakea ryhmäkanavaa" + }, + { + "id": "app.import.import_direct_post.create_direct_channel.error", + "translation": "Ei voitu hakea suorakanavaa" + }, + { + "id": "app.import.import_direct_channel.update_header_failed.error", + "translation": "Ei voitu muuttaa suorakanavan otsikkoa" + }, + { + "id": "app.import.import_direct_channel.create_group_channel.error", + "translation": "Ei voitu luoda ryhmäkanavaa" + }, + { + "id": "app.import.import_direct_channel.create_direct_channel.error", + "translation": "Ei voitu luoda suorakanavaa" + }, + { + "id": "app.import.import_channel.team_not_found.error", + "translation": "Virhe tuotaessa kanavaa. Tiimiä nimeltä \"{{.TeamName}}\" ei löydetty." + }, + { + "id": "app.import.import_channel.scheme_wrong_scope.error", + "translation": "Kanava on asetettava Kanava-skoopatulle järjestelylle." + }, + { + "id": "app.import.import_channel.scheme_deleted.error", + "translation": "Ei voitu asettaa kanava käyttämään poistettua järjestelyä." + }, + { + "id": "app.import.emoji.bad_file.error", + "translation": "Virhe luettaessa tuotavaa emoji-kuvatiedostoa. Emojin nimi on: \"{{.EmojiName}}\"" + }, + { + "id": "app.import.bulk_import.unsupported_version.error", + "translation": "Datan tuontitiedostossa virheellinen tai puuttuva versio. Tarkista että versio on ensimmäinen objekti tuontitiedostossa ja yritä uudestaan." + }, + { + "id": "app.import.bulk_import.json_decode.error", + "translation": "Rivin JSON-dekoodaus epäonnistui." + }, + { + "id": "app.import.bulk_import.file_scan.error", + "translation": "Virhe luettaessa tuotavaa datatiedostoa." + }, + { + "id": "app.import.attachment.file_upload.error", + "translation": "Virhe ladattaessa tiedostoa: \"{{.FilePath}}\"" + }, + { + "id": "app.import.attachment.bad_file.error", + "translation": "Virhe luettaessa tiedostoa: \"{{.FilePath}}\"" + }, + { + "id": "app.export.export_write_line.json_marshall.error", + "translation": "Tapahtui virhe muutettaessa vietävää dataa JSON-muotoon." + }, + { + "id": "app.export.export_write_line.io_writer.error", + "translation": "Tapahtui virhe kirjoitettaessa vientidataa." + }, + { + "id": "app.channel.post_update_channel_purpose_message.updated_to", + "translation": "%s päivitti kanavan tarkoitukseksi: %s" + }, + { + "id": "app.channel.post_update_channel_purpose_message.updated_from", + "translation": "%s päivitti kanavan kuvauksen %s, uusi kuvaus: %s" + }, + { + "id": "app.channel.post_update_channel_purpose_message.retrieve_user.error", + "translation": "Käyttäjän hakeminen kanavaotsikon päivittämisen aikana epäonnistui %v" + }, + { + "id": "app.channel.post_update_channel_purpose_message.removed", + "translation": "%s poisti kanavaotsikon (oli: %s)" + }, + { + "id": "app.channel.post_update_channel_purpose_message.post.error", + "translation": "Kanavan merkitys-viestin lähettäminen epäonnistui" + }, + { + "id": "app.channel.move_channel.members_do_not_match.error", + "translation": "Kanavaa ei voi siirtää elleivät kaikki sen jäsenet ole kohdetiimin jäseniä." + }, + { + "id": "app.channel.create_direct_channel.internal_error", + "translation": "Ei voitu tallentaa kanavaa" + }, + { + "id": "app.channel.create_channel.no_team_id.app_error", + "translation": "Tiimin ID on määritettävä kanavaa luotaessa" + }, + { + "id": "app.channel.create_channel.internal_error", + "translation": "Ei voitu tallentaa kanavaa" + }, + { + "id": "app.bot.permenent_delete.bad_id", + "translation": "Ei voitu poistaa viestiä" + }, + { + "id": "app.bot.patchbot.internal_error", + "translation": "Ei voida päivittää viestiä" + }, + { + "id": "app.bot.getbots.internal_error", + "translation": "Ei voitu hakea viestejä" + }, + { + "id": "app.bot.getbot.internal_error", + "translation": "Ei voitu hakea viestiä" + }, + { + "id": "app.bot.createbot.internal_error", + "translation": "Ei voitu tallentaa viestiä" + }, + { + "id": "app.admin.test_email.failure", + "translation": "Yhteys epäonnistui: {{.Error}}" + }, + { + "id": "api.websocket_handler.invalid_param.app_error", + "translation": "Virheellinen {{.Name}} parametri" + }, + { + "id": "api.webhook.update_outgoing.intersect.app_error", + "translation": "Uloslähtevät webhookeilla samasta kanavasta ei voi olla samoja laukaisusanoja/callback-URLeja." + }, + { + "id": "api.webhook.team_mismatch.app_error", + "translation": "Ei voida päivittää webhookia tiimien välillä" + }, + { + "id": "api.webhook.incoming.error", + "translation": "Ei voitu dekoodata sisääntulevan webhookin moniosaista sisältöä." + }, + { + "id": "api.webhook.create_outgoing.triggers.app_error", + "translation": "Joko trigger_words tai channel_id on asetettava" + }, + { + "id": "api.webhook.create_outgoing.permissions.app_error", + "translation": "Virheelliset oikeidet luoda uloslähtevä webhook." + }, + { + "id": "api.webhook.create_outgoing.not_open.app_error", + "translation": "Uloslähteviä webhookeja voi luoda vain julkisille kanaville." + }, + { + "id": "api.webhook.create_outgoing.intersect.app_error", + "translation": "Uloslähtevät webhookeilla samasta kanavasta ei voi olla samoja laukaisusanoja/callback-URLeja." + }, + { + "id": "api.web_socket_router.not_authenticated.app_error", + "translation": "WebSocket-yhteys ei ole autentikoitu. Kirjaudu sisään ja yritä uudestaan." + }, + { + "id": "api.web_socket_router.no_action.app_error", + "translation": "Ei websocket-toimintoa." + }, + { + "id": "api.web_socket_router.bad_seq.app_error", + "translation": "Virheellinen WebSocket-viestin sekvenssi." + }, + { + "id": "api.web_socket_router.bad_action.app_error", + "translation": "Tuntematon WebSocket-toiminto." + }, + { + "id": "api.web_socket.connect.upgrade.app_error", + "translation": "Ei voitu parantaa websocket-yhteyttä" + }, + { + "id": "api.user.verify_email.token_parse.error", + "translation": "Ei voitu parsia token dataa sähköpostivahvistuksesta" + }, + { + "id": "api.user.verify_email.link_expired.app_error", + "translation": "Sähköpostin vahvistuslinkki on vanhentunut." + }, + { + "id": "api.user.verify_email.broken_token.app_error", + "translation": "Virheellinen sähköpostin vahvistus token tyyppi." + }, + { + "id": "api.user.verify_email.bad_link.app_error", + "translation": "Virheellinen sähköpostin vahvistuslinkki." + }, + { + "id": "api.user.upload_profile_user.upload_profile.app_error", + "translation": "Emme pystyneet lataamaan profiilikuvaa" + }, + { + "id": "api.user.upload_profile_user.too_large.app_error", + "translation": "Profiilikuvan lataaminen epäonnistui. Tiedosto on liian suuri." + }, + { + "id": "api.user.upload_profile_user.storage.app_error", + "translation": "Kuvan lataaminen epäonnistui. Kuvien tallennuksen asetuksia ei ole tehty." + }, + { + "id": "api.user.upload_profile_user.parse.app_error", + "translation": "Moniosaista lomaketta ei voitu jäsentää" + }, + { + "id": "api.user.upload_profile_user.open.app_error", + "translation": "Kuvatiedostoa ei voitu avata" + }, + { + "id": "api.user.upload_profile_user.no_file.app_error", + "translation": "Ei tiedostoa pyynnön kohdassa 'image'." + }, + { + "id": "api.user.upload_profile_user.encode.app_error", + "translation": "Ei voitu enkoodata profiilikuvaa" + }, + { + "id": "api.user.upload_profile_user.decode.app_error", + "translation": "Ei voitu dekoodata profiilikuvaa" + }, + { + "id": "api.user.upload_profile_user.array.app_error", + "translation": "Tyhjä taulukko pyynnön kohdassa 'image'" + }, + { + "id": "api.user.update_user.accepted_guest_domain.app_error", + "translation": "Sähköpostiosoitteesi ei kuulu hyväksyttyyn domainiin. Ota yhteyttä järjestelmänvalvojaan tai liity toisella sähköpostiosoitteella." + }, + { + "id": "api.user.update_user.accepted_domain.app_error", + "translation": "Sähköpostiosoitteesi ei kuulu hyväksyttyyn domainiin. Ota yhteyttä järjestelmänvalvojaan tai liity toisella sähköpostiosoitteella." + }, + { + "id": "api.user.update_password.valid_account.app_error", + "translation": "Salasanan muutos epäonnistui koska emme löytäneet voimassaolevaa tiliä" + }, + { + "id": "api.user.update_password.oauth.app_error", + "translation": "Salasanan muutos epäonnistui koska käyttäjä on kirjautunut OAuth-palvelun kautta" + }, + { + "id": "api.user.update_password.menu", + "translation": "käyttäen asetusvalikkoa" + }, + { + "id": "api.user.update_password.incorrect.app_error", + "translation": "Syöttämäsi \"Nykyinen salasana\" ei ole oikein. Tarkista että Caps Lock on pois päältä ja yritä uudestaan." + }, + { + "id": "api.user.update_password.failed.app_error", + "translation": "Salasanan muutos epäonnistui" + }, + { + "id": "api.user.update_password.context.app_error", + "translation": "Salasanan muutos epäonnistui koska kontekstin user_id ei ole sama kuin propin user_id" + }, + { + "id": "api.user.update_oauth_user_attrs.get_user.app_error", + "translation": "Ei voitu noutaa käyttäjää {{.Service}} käyttäjäobjektista" + }, + { + "id": "api.user.update_active.permissions.app_error", + "translation": "Sinulla ei ole tarvittavia oikeuksia" + }, + { + "id": "api.user.update_active.not_enable.app_error", + "translation": "Et voi deaktivoida itseäsi koska tämä toiminto on kytketty pois. Ota yhteys järjestelmänvalvojaan." + }, + { + "id": "api.user.send_verify_email_and_forget.failed.error", + "translation": "Ei voitu lähettää vahvistussähköpostiviestiä" + }, + { + "id": "api.user.send_sign_in_change_email_and_forget.error", + "translation": "Ei voitu lähettää salasanan muutossähköpostia" + }, + { + "id": "api.user.send_password_reset.sso.app_error", + "translation": "Salasanaa ei voida palauttaa SSO tileille" + }, + { + "id": "api.user.send_password_reset.send.app_error", + "translation": "Ei voitus lähettää salasanan palautussähköpostia" + }, + { + "id": "api.user.send_email_change_verify_email_and_forget.error", + "translation": "Ei voitu lähettää sähköpostin muutosvahvistusviestiä" + }, + { + "id": "api.user.send_deactivate_email_and_forget.failed.error", + "translation": "Ei voitu lähettää tilin deaktivointisähköpostiviestiä" + }, + { + "id": "api.user.saml.not_available.app_error", + "translation": "SAML 2.0 ei ole konfiguroitu tai ei ole tuettu tällä palvelimella." + }, + { + "id": "api.user.reset_password.sso.app_error", + "translation": "Salasanaa ei voida palauttaa SSO tileille" + }, + { + "id": "api.user.reset_password.method", + "translation": "käyttäen salasanan palautuslinkkiä" + }, + { + "id": "api.user.reset_password.link_expired.app_error", + "translation": "Salasanan palautuslinkki on vanhentunut." + }, + { + "id": "api.user.reset_password.invalid_link.app_error", + "translation": "Salasanan palautuslinkki ei ole kelvollinen." + }, + { + "id": "api.user.reset_password.broken_token.app_error", + "translation": "Salasanan palautus-token ei ole kelvollinen." + }, + { + "id": "api.user.oauth_to_email.not_available.app_error", + "translation": "Autentikointisiirtoa ei määritetty tai ei tarjolla palvelimella." + }, + { + "id": "api.user.oauth_to_email.context.app_error", + "translation": "Salasanan muutos epäonnistui koska kontekstin user_id ei ole sama kuin annettu käyttäjän id" + }, + { + "id": "api.user.login_ldap.not_available.app_error", + "translation": "AD/LDAP ei ole saatavilla tällä palvelimella" + }, + { + "id": "api.user.login_by_oauth.parse.app_error", + "translation": "Ei voitu lukea auth dataa {{.Service}} käyttäjäobjektista" + }, + { + "id": "api.templates.reset_body.button", + "translation": "Palauta salasana" + }, + { + "id": "api.templates.post_body.button", + "translation": "Siirry viestiin" + }, + { + "id": "api.templates.password_change_subject", + "translation": "[{{ .SiteName }}] Salasanasi on päivitetty" + }, + { + "id": "api.templates.password_change_body.title", + "translation": "Salasanasi on päivitetty" + }, + { + "id": "api.templates.password_change_body.info", + "translation": "Salasanasi on päivitetty tiimissä {{.TeamDisplayName}} osoitteessa {{ .TeamURL }} toiminnolla {{.Method}}." + }, + { + "id": "api.templates.mfa_deactivated_body.title", + "translation": "Kaksivaiheinen tunnistautuminen on poistettu" + }, + { + "id": "api.templates.mfa_deactivated_body.info", + "translation": "Kaksivaiheinen tunnistautuminen on poistettu tililtäsi osoitteessa {{ .SiteURL }}." + }, + { + "id": "api.templates.mfa_change_subject", + "translation": "[{{ .SiteName }}] MFA:si on päivitetty" + }, + { + "id": "api.templates.mfa_activated_body.title", + "translation": "Kaksivaiheinen tunnistautuminen on lisätty" + }, + { + "id": "api.templates.mfa_activated_body.info", + "translation": "Kaksivaiheinen tunnistautuminen on lisätty tilillesi osoitteessa {{ .SiteURL }}." + }, + { + "id": "api.templates.invite_subject", + "translation": "[{{ .SiteName }}] {{ .SenderName }} Kutsui sinut liittymään tiimiin {{ .TeamDisplayName }}" + }, + { + "id": "api.templates.invite_guest_subject", + "translation": "[{{ .SiteName }}] {{ .SenderName }} Kutsui sinut liittymään tiimiin {{ .TeamDisplayName }}" + }, + { + "id": "api.templates.invite_body.title", + "translation": "Sinut on kutsuttu" + }, + { + "id": "api.templates.invite_body.button", + "translation": "Liity tiimiin" + }, + { + "id": "api.templates.email_warning", + "translation": "Jos et tehnyt tätä muutosta, ota yhteys järjestelmänvalvojaan." + }, + { + "id": "api.templates.email_organization", + "translation": "Lähettänyt " + }, + { + "id": "api.templates.email_info3", + "translation": "{{.SiteName}} tiimi" + }, + { + "id": "api.templates.email_info2", + "translation": "Parhain terveisin," + }, + { + "id": "api.templates.email_info1", + "translation": "Kysymyksiä, lähetä meille postia: " + }, + { + "id": "api.templates.email_footer", + "translation": "Muuttaaksesi ilmoitusasetuksiasi, kirjaudu tiimisi sivulle ja mene Tilin asetukset > Ilmoitukset." + }, + { + "id": "api.templates.email_change_verify_subject", + "translation": "[{{ .SiteName }}] Vahvista uusi sähköpostiosoite" + }, + { + "id": "api.templates.email_change_verify_body.title", + "translation": "Muutit sähköpostiosoitteesi" + }, + { + "id": "api.templates.email_change_verify_body.info", + "translation": "Viimeistele sähköpostiosoitteen muutos tiimissä {{.TeamDisplayName}} klikkaamalla alla olevaa linkkiä joka vahvistaa että tämä on oikea osoite." + }, + { + "id": "api.templates.email_change_verify_body.button", + "translation": "Vahvista sähköposti" + }, + { + "id": "api.templates.email_change_subject", + "translation": "[{{ .SiteName }}] Sähköpostiosoitteesi on muutettu" + }, + { + "id": "api.templates.email_change_body.title", + "translation": "Päivitit sähköpostisi" + }, + { + "id": "api.templates.email_change_body.info", + "translation": "Sähköpostiosoitteesi tiimissä {{.TeamDisplayName}} on muutettu muotoon {{.NewEmail}}." + }, + { + "id": "api.templates.deactivate_subject", + "translation": "[{{ .SiteName }}] Tilisi osoitteessa {{ .ServerURL }} on kytketty pois käytöstä" + }, + { + "id": "api.templates.deactivate_body.warning", + "translation": "Jos et suorittanut tätä toimintoa tai haluat aktivoida tilisi, ota yhteys järjestelmänvalvojaan." + }, + { + "id": "api.templates.deactivate_body.title", + "translation": "Tilisi on kytketty pois käytöstä osoitteessa {{ .ServerURL }}" + }, + { + "id": "api.templates.deactivate_body.info", + "translation": "Kytkit tilisi pois käytöstä osoitteessa {{ .SiteURL }}." + }, + { + "id": "api.team.update_team_scheme.scheme_scope.error", + "translation": "Ei voitu asettaa järjestelyä tiimille koska annettu järjestely ei ole tiimijärjestely." + }, + { + "id": "api.team.update_team_scheme.license.error", + "translation": "Lisenssisi ei tue tiimin järjestelyn muuttamista" + }, + { + "id": "api.team.update_restricted_domains.mismatch.app_error", + "translation": "Tiimin rajoittaminen domainiin {{ .Domain }} ei ole sallittu järjestelmän asetuksissa. Ota yhteys järjestelmänvalvojaan." + }, + { + "id": "api.team.update_member_roles.not_a_member", + "translation": "Käyttäjä ei ole tiimin jäsen." + }, + { + "id": "api.team.team_icon.update.app_error", + "translation": "Tiimin ikonin muuttamisessa tapahtui virhe" + }, + { + "id": "api.team.set_team_icon.write_file.app_error", + "translation": "Ei voitu tallentaa tiimin ikonia" + }, + { + "id": "api.team.set_team_icon.too_large.app_error", + "translation": "Tiimin ikonin lataaminen epäonnistui. Tiedosto on liian suuri." + }, + { + "id": "api.team.set_team_icon.storage.app_error", + "translation": "Tiimin ikonin lataaminen epäonnistui. Kuvien tallennuksen asetuksia ei ole tehty." + }, + { + "id": "api.team.set_team_icon.parse.app_error", + "translation": "Moniosaista lomaketta ei voitu jäsentää" + }, + { + "id": "api.team.set_team_icon.open.app_error", + "translation": "Kuvatiedostoa ei voitu avata" + }, + { + "id": "api.team.set_team_icon.no_file.app_error", + "translation": "Ei tiedostoa pyynnön kohdassa 'image'." + }, + { + "id": "api.team.set_team_icon.get_team.app_error", + "translation": "Tiimiä haettaessa tapahtui virhe" + }, + { + "id": "api.team.set_team_icon.encode.app_error", + "translation": "Ei voitu enkoodata tiimin ikonia" + }, + { + "id": "api.team.set_team_icon.decode.app_error", + "translation": "Ei voitu lukea tiimin ikonia" + }, + { + "id": "api.team.set_team_icon.array.app_error", + "translation": "Tyhjä taulukko pyynnön kohdassa 'image'" + }, + { + "id": "api.team.remove_user_from_team.removed", + "translation": "%v poistettiin tiimistä." + }, + { + "id": "api.team.remove_user_from_team.missing.app_error", + "translation": "Käyttäjä ei ole osa tätä tiimiä." + }, + { + "id": "api.team.remove_team_icon.get_team.app_error", + "translation": "Tiimiä haettaessa tapahtui virhe" + }, + { + "id": "api.team.move_channel.success", + "translation": "Tämä kanava on siirretty tähän tiimiin tiimistä %v." + }, + { + "id": "api.team.move_channel.post.error", + "translation": "Kanavan siirtoviestin lähettäminen epäonnistui." + }, + { + "id": "api.team.leave.left", + "translation": "%v poistui tiimistä." + }, + { + "id": "api.team.join_team.post_and_forget", + "translation": "%v liittyi tiimiin." + }, + { + "id": "api.team.is_team_creation_allowed.disabled.app_error", + "translation": "Tiimin luonti on kytketty pois. Pyydä järjestelmänvalvojalta lisätietoja." + }, + { + "id": "api.team.invite_members.no_one.app_error", + "translation": "Ei ketään kutsuttavaa." + }, + { + "id": "api.team.invite_members.invalid_email.app_error", + "translation": "Seuraavat sähköpostiosoitteet eivät kuulu hyväksyttyyn domainiin: {{.Addresses}}. Ota yhteyttä järjestelmänvalvojaan lisätietoja varten." + }, + { + "id": "api.team.invite_members.disabled.app_error", + "translation": "Sähköpostikutsut on kytketty pois." + }, + { + "id": "api.team.import_team.unavailable.app_error", + "translation": "Virheellinen pyyntö: filesize kenttää ei löydetty." + }, + { + "id": "api.team.import_team.parse.app_error", + "translation": "Moniosaista lomaketta ei voitu jäsentää" + }, + { + "id": "api.team.import_team.open.app_error", + "translation": "Tiedostoa ei voitu avata" + }, + { + "id": "api.team.import_team.no_import_from.app_error", + "translation": "Virheellinen pyyntö: importFrom kenttää ei löydetty." + }, + { + "id": "api.team.import_team.no_file.app_error", + "translation": "Ei tiedostoa pyynnön kohdassa 'file'" + }, + { + "id": "api.team.import_team.integer.app_error", + "translation": "Filesize ei ole kokonaisluku" + }, + { + "id": "api.team.import_team.array.app_error", + "translation": "Tyhjä taulukko pyynnön kohdassa 'file'" + }, + { + "id": "api.team.get_team_icon.read_file.app_error", + "translation": "Ei voitu lukea tiimin ikonitiedostoa." + }, + { + "id": "api.team.get_team_icon.filesettings_no_driver.app_error", + "translation": "Virheellinen ajurinimi tiedostoasetuksille. On oltava 'local' tai 'amazons3'" + }, + { + "id": "api.team.get_invite_info.not_open_team", + "translation": "Kutsu ei ole voimassa koska tämä ei ole avoin tiimi." + }, + { + "id": "api.team.get_all_teams.insufficient_permissions", + "translation": "Sinulla ei ole oikeuksia muokata kanavan otsikkoa." + }, + { + "id": "api.team.demote_user_to_guest.license.error", + "translation": "Lisenssisi ei tue vierailijatilejä" + }, + { + "id": "api.team.add_user_to_team.missing_parameter.app_error", + "translation": "Parametri vaaditaan käyttäjän lisäämiseksi tiimiin." + }, + { + "id": "api.team.add_user_to_team.added", + "translation": "%v lisättiin kanavalle, lisääjänä %v." + }, + { + "id": "api.team.add_members.error", + "translation": "Tapahtui virhe jäsen(i)en lisäämisessä." + }, + { + "id": "api.status.user_not_found.app_error", + "translation": "Käyttäjää ei löydetty" + }, + { + "id": "api.slackimport.slack_import.zip.app_error", + "translation": "Ei voitu avata Slack export zip tiedostoa.\r\n" + }, + { + "id": "api.slackimport.slack_import.team_fail", + "translation": "Ei voitu hakea tiimia johon tuodaan.\r\n" + }, + { + "id": "api.slackimport.slack_import.open.app_error", + "translation": "Ei voitu avata tiedostoa: {{.Filename}}.\r\n" + }, + { + "id": "api.slackimport.slack_import.notes", + "translation": "\r\nMuistiinpanot:\r\n" + }, + { + "id": "api.slackimport.slack_import.note3", + "translation": "- Lisää virheitä voi löytää palvelimen logista.\r\n" + }, + { + "id": "api.slackimport.slack_import.note2", + "translation": "- Slack bot viestejä ei tällä hetkellä tueta.\r\n" + }, + { + "id": "api.slackimport.slack_import.note1", + "translation": "- Joitakin viestejä ei tuotu koska tämä tuontityökalu ei tue niitä.\r\n" + }, + { + "id": "api.slackimport.slack_import.log", + "translation": "Mattermost Slack tuontilogi\r\n" + }, + { + "id": "api.slackimport.slack_add_users.unable_import", + "translation": "Ei voitu tuoda Slack-käyttäjää: {{.Username}}.\r\n" + }, + { + "id": "api.slackimport.slack_add_users.missing_email_address", + "translation": "Käyttäjällä {{.Username}} ei ole sähköpostiosoitetta Slack-exportissa. Käytetty {{.Email}} sen paikalla. Käyttäjän tulisi päivittää sähköpostiosoitteensa kun hän on kirjautunut järjestelmään.\r\n" + }, + { + "id": "api.slackimport.slack_add_users.merge_existing_failed", + "translation": "Slack-käyttäjä yhdistetty olemassaolevaan Mattermost-käyttäjään jolla sama sähköpostiosoite {{.Email}} ja käyttäjänimi {{.Username}}, mutta ei voitu lisätä käyttäjää tiimiin.\r\n" + }, + { + "id": "app.channel.get_deleted.missing.app_error", + "translation": "Poistettuja kanavia ei olemassa" + }, + { + "id": "app.channel.get_deleted.existing.app_error", + "translation": "Ei löydetty olemassaolevaa poistettua kanavaa" + }, + { + "id": "app.channel.get_channels.not_found.app_error", + "translation": "Kanavia ei löytynyt" + }, + { + "id": "app.channel.get_by_name.missing.app_error", + "translation": "Kanavaa ei ole olemassa" + }, + { + "id": "store.sql_channel.get.existing.app_error", + "translation": "Ei löydetty olemassaolevaa kanavaa" + }, + { + "id": "ent.ldap.app_error", + "translation": "ldap-rajapinta oli puuttuva" + }, + { + "id": "ent.elasticsearch.test_config.reenter_password", + "translation": "Elasticsearch palvelun URL tai käyttäjänimi on muuttunut. Syötä Elasticsearch salasana yhteyden testausta varten." + }, + { + "id": "ent.elasticsearch.test_config.license.error", + "translation": "Lisenssisi ei tue Elasticsearchia." + }, + { + "id": "ent.elasticsearch.test_config.indexing_disabled.error", + "translation": "Elasticsearch on kytketty pois." + }, + { + "id": "ent.elasticsearch.stop.already_stopped.app_error", + "translation": "Elasticsearch on jo lopetettu" + }, + { + "id": "ent.elasticsearch.start.start_bulk_processor_failed.app_error", + "translation": "Virhe käynnistettäessä Elasticsearch bulkkiprosessori" + }, + { + "id": "ent.elasticsearch.start.create_bulk_processor_failed.app_error", + "translation": "Virhe luotaessa Elasticsearch bulkkiprosessoria" + }, + { + "id": "ent.elasticsearch.start.already_started.app_error", + "translation": "Elasticsearch on jo käynnistetty" + }, + { + "id": "ent.elasticsearch.search_users.unmarshall_user_failed", + "translation": "Hakutulosten dekoodaus epäonnistui" + }, + { + "id": "ent.elasticsearch.search_users.search_failed", + "translation": "Haku ei valmistunut" + }, + { + "id": "ent.elasticsearch.search_posts.unmarshall_post_failed", + "translation": "Hakutulosten dekoodaus epäonnistui" + }, + { + "id": "ent.elasticsearch.search_posts.search_failed", + "translation": "Haku ei valmistunut" + }, + { + "id": "ent.elasticsearch.search_posts.parse_matches_failed", + "translation": "Hakutulosten mätsäysten parsiminen epäonnistui" + }, + { + "id": "ent.elasticsearch.search_posts.disabled", + "translation": "Elasticsearch-haut poissa käytöstä tällä palvelimella" + }, + { + "id": "ent.elasticsearch.search_channels.unmarshall_channel_failed", + "translation": "Hakutulosten dekoodaus epäonnistui" + }, + { + "id": "ent.elasticsearch.search_channels.search_failed", + "translation": "Haku ei valmistunut" + }, + { + "id": "ent.elasticsearch.search_channels.disabled", + "translation": "Elasticsearch-haut poissa käytöstä tällä palvelimella" + }, + { + "id": "ent.elasticsearch.refresh_indexes.refresh_failed", + "translation": "Elasticsearch-indeksin haku epäonnistui" + }, + { + "id": "ent.elasticsearch.purge_indexes.delete_failed", + "translation": "Elasticsearch-indeksin poisto epäonnistui" + }, + { + "id": "ent.elasticsearch.not_started.error", + "translation": "Elasticsearch ei ole käynnistetty" + }, + { + "id": "ent.elasticsearch.indexer.do_job.parse_start_time.error", + "translation": "Elasticsearch-indeksointityöskentelijä ei voinut parsia aloitusaikaa" + }, + { + "id": "ent.elasticsearch.indexer.do_job.parse_end_time.error", + "translation": "Elasticsearch-indeksointityöskentelijä ei voinut parsia loppuaikaa" + }, + { + "id": "ent.elasticsearch.index_user.error", + "translation": "Viestin indeksointi epäonnistui" + }, + { + "id": "ent.elasticsearch.index_post.error", + "translation": "Viestin indeksointi epäonnistui" + }, + { + "id": "ent.elasticsearch.index_channel.error", + "translation": "Kanavaa ei voitu päivittää" + }, + { + "id": "ent.elasticsearch.generic.disabled", + "translation": "Elasticsearch-haku ei ole käytössä tällä palvelimella" + }, + { + "id": "ent.elasticsearch.delete_user.error", + "translation": "Viestin poisto epäonnistui" + }, + { + "id": "ent.elasticsearch.delete_post.error", + "translation": "Viestin poisto epäonnistui" + }, + { + "id": "ent.elasticsearch.delete_channel.error", + "translation": "Kanavaa ei voitu päivittää" + }, + { + "id": "ent.elasticsearch.data_retention_delete_indexes.get_indexes.error", + "translation": "Elasticsearch-indeksin haku epäonnistui" + }, + { + "id": "ent.elasticsearch.data_retention_delete_indexes.delete_index.error", + "translation": "Elasticsearch-indeksin poisto epäonnistui" + }, + { + "id": "ent.elasticsearch.create_client.connect_failed", + "translation": "Elasticsearch-asiakkaan asettaminen epäonnistui" + }, + { + "id": "ent.elasticsearch.aggregator_worker.index_job_failed.error", + "translation": "Elasticsearch-aggregaattorityöskentelijä ei onnistunut koska ideksointityö epäonnistui" + }, + { + "id": "ent.elasticsearch.aggregator_worker.get_indexes.error", + "translation": "Elasticsearch-aggregaattorityöskentelijä ei onnistunut hakemaan indeksejä" + }, + { + "id": "ent.elasticsearch.aggregator_worker.delete_indexes.error", + "translation": "Elasticsearch-aggregaattorityöskentelijä ei onnistunut poistamaan indeksejä" + }, + { + "id": "ent.elasticsearch.aggregator_worker.create_index_job.error", + "translation": "Elasticsearch-aggregaattorityöskentelijä ei onnistunut luomaan indeksointityötä" + }, + { + "id": "ent.data_retention.generic.license.error", + "translation": "Lisenssisi ei tue Tiedonsäilytystä." + }, + { + "id": "ent.compliance.run_failed.error", + "translation": "Compliance-vienti epäonnistui työlle '{{.JobName}}' sijainnissa '{{.FilePath}}'" + }, + { + "id": "ent.compliance.run_export.template_watcher.appError", + "translation": "Ei voitu ladata vientimalleja. Yritä uudestaan." + }, + { + "id": "ent.compliance.licence_disable.app_error", + "translation": "Compliance-toiminnallisuus on kytketty pois nykyisessä lisenssissä. Kysy järjestelmänvalvojalta Enterprise-lisenssiin vaihtamisesta." + }, + { + "id": "ent.compliance.global_relay.rewind_temporary_file.appError", + "translation": "Ei voitu uudelleenlukea väliaikaista Global Relay vientitiedostoa." + }, + { + "id": "ent.compliance.global_relay.open_temporary_file.appError", + "translation": "Ei voitu avata väliaikaista vientitiedostoa." + }, + { + "id": "ent.compliance.global_relay.attachments_removed.appError", + "translation": "Ladattu kuva poistettiin Global Relay viennistä koska se oli liian suuri lähetettäväksi." + }, + { + "id": "ent.compliance.csv.zip.creation.appError", + "translation": "Ei voitu luoda viennin zip-tiedostoa." + }, + { + "id": "ent.compliance.csv.warning.appError", + "translation": "Ei voitu luoda eml tiedostoa." + }, + { + "id": "ent.compliance.csv.post.export.appError", + "translation": "Ei voitu viedä viestiä." + }, + { + "id": "ent.compliance.csv.metadata.json.zipfile.appError", + "translation": "Ei voitu luoda eml tiedostoa." + }, + { + "id": "ent.compliance.csv.metadata.json.marshalling.appError", + "translation": "Ei voitu muuttaa metadataa JSON-muotoon." + }, + { + "id": "ent.compliance.csv.metadata.export.appError", + "translation": "Ei voitu lisätä metadatatiedostoa zip-tiedostoon." + }, + { + "id": "ent.compliance.csv.header.export.appError", + "translation": "Ei voitu lisätä otsikkoa CSV-vientiin." + }, + { + "id": "ent.compliance.csv.file.creation.appError", + "translation": "Ei voitu luoda väliaikaista CSV-vientitiedostoa." + }, + { + "id": "ent.compliance.csv.attachment.export.appError", + "translation": "Ei voitu lisätä liitettä CSV-vientiin." + }, + { + "id": "ent.compliance.csv.attachment.copy.appError", + "translation": "Ei voitu kopioida liitettä zip-tiedostoon." + }, + { + "id": "ent.compliance.bad_export_type.appError", + "translation": "Tuntematon vientimuoto {{.ExportType}}" + }, + { + "id": "ent.cluster.save_config.error", + "translation": "Järjestelmäkonsoli on vain-luku-tilassa kun High Availability on valittu, ellei ReadOnlyConfig ole kytketty pois asetustiedostossa." + }, + { + "id": "ent.cluster.config_changed.info", + "translation": "Klusterin asetukset ovat muuttuneet noodille id={{ .id }}. Klusteri voi tulla epävarmaksi ja uudelleenkäynnistys vaaditaan. Varmistaaksesi että klusterin asetukset ovat kunnossa sinun kannattaa suorittaa rullaava uudelleenkäynnistys heti." + }, + { + "id": "ent.account_migration.get_saml_users_failed", + "translation": "SAML-käyttäjiä ei voitu hakea." + }, + { + "id": "ent.account_migration.get_all_failed", + "translation": "Käyttäjiä ei voitu hakea." + }, + { + "id": "brand.save_brand_image.save_image.app_error", + "translation": "Ei voitu kirjoittaa kuvatiedostoa tiedostovarastoon. Tarkista yhteys ja yritä uudestaan." + }, + { + "id": "brand.save_brand_image.open.app_error", + "translation": "Ei voitu ladata omaa brändikuvaa. Tarkista että kuvan koko on alle 2 Mt ja yritä uudestaan." + }, + { + "id": "brand.save_brand_image.encode.app_error", + "translation": "Ei voitu muuttaa kuvadataa PNG muotoon. Yritä uudestaan." + }, + { + "id": "brand.save_brand_image.decode.app_error", + "translation": "Ei voitu dekoodata kuvadataa." + }, + { + "id": "bleveengine.search_users_in_team.error", + "translation": "Haku ei valmistunut" + }, + { + "id": "bleveengine.search_users_in_channel.uchan.error", + "translation": "Haku ei valmistunut" + }, + { + "id": "bleveengine.search_users_in_channel.nuchan.error", + "translation": "Haku ei valmistunut" + }, + { + "id": "bleveengine.search_posts.error", + "translation": "Haku ei valmistunut" + }, + { + "id": "bleveengine.search_channels.error", + "translation": "Haku ei valmistunut" + }, + { + "id": "bleveengine.indexer.do_job.parse_start_time.error", + "translation": "Elasticsearch-indeksointityöskentelijä ei voinut parsia aloitusaikaa" + }, + { + "id": "bleveengine.indexer.do_job.parse_end_time.error", + "translation": "Elasticsearch-indeksointityöskentelijä ei voinut parsia loppuaikaa" + }, + { + "id": "bleveengine.index_user.error", + "translation": "Viestin indeksointi epäonnistui" + }, + { + "id": "bleveengine.index_post.error", + "translation": "Viestin indeksointi epäonnistui" + }, + { + "id": "bleveengine.index_channel.error", + "translation": "Kanavaa ei voitu päivittää" + }, + { + "id": "bleveengine.delete_user.error", + "translation": "Viestin poisto epäonnistui" + }, + { + "id": "bleveengine.delete_post.error", + "translation": "Viestin poisto epäonnistui" + }, + { + "id": "bleveengine.delete_channel.error", + "translation": "Kanavaa ei voitu päivittää" + }, + { + "id": "app.user_access_token.invalid_or_missing", + "translation": "Virheellinen tai puuttuva token" + }, + { + "id": "app.user_access_token.disabled", + "translation": "Henkilökohtaiset access tokenit on kytketty pois tällä palvelimella. Kysy järjestelmänvalvojalta lisätietoja." + }, + { + "id": "app.team.rename_team.name_occupied", + "translation": "Tiimin nimeä ei voitu vaihtaa, nimi on jo käytössä" + }, + { + "id": "app.team.join_user_to_team.max_accounts.app_error", + "translation": "Tiimin jäsenmäärä on saavuttanut maksimirajan. Ota yhteys järjestelmänvalvojaan rajan korottamiseksi." + }, + { + "id": "app.system_install_date.parse_int.app_error", + "translation": "Ei voitu lukea asennuspäiväystä" + }, + { + "id": "app.submit_interactive_dialog.json_error", + "translation": "Interaktiivisen dialogin JSON enkoodauksen aikana kohdattiin virhe." + }, + { + "id": "app.schemes.is_phase_2_migration_completed.not_completed.app_error", + "translation": "Tämä API rajapinta ei ole käytettävissä koska tarvittavia migraatioita ei ole vielä suorittu valmiiksi." + }, + { + "id": "app.save_config.app_error", + "translation": "Tapahtui virhe tallennettaessa konfiguraatiota" + }, + { + "id": "app.role.check_roles_exist.role_not_found", + "translation": "Annettua roolia ei ole olemassa" + }, + { + "id": "app.plugin.write_file.saving.app_error", + "translation": "Virhe tapahtui kanavalle poistumisen aikana." + }, + { + "id": "app.plugin.write_file.read.app_error", + "translation": "Virhe tapahtui kanavalle poistumisen aikana." + }, + { + "id": "app.plugin.upload_disabled.app_error", + "translation": "Lisäosat ja/tai lisäosien tiedostolatauksen on kytketty pois." + }, + { + "id": "app.plugin.sync.read_local_folder.app_error", + "translation": "Virhe luettaessa paikallista liitännäisten hakemistoa." + }, + { + "id": "app.plugin.remove.app_error", + "translation": "Ei voitu poistaa lisäosaa" + }, + { + "id": "app.plugin.not_installed.app_error", + "translation": "Lisäosaa ei asennettu" + }, + { + "id": "app.plugin.mvdir.app_error", + "translation": "Ei voitu siirtää lisäosaa väliaikaishakemistosta kohteeseen. Toinen lisäosa saattaa käyttää samaa hakemistonimeä." + }, + { + "id": "app.plugin.marketplace_disabled.app_error", + "translation": "Lisäosat on kytketty pois. Tarkista lokista lisätiedot." + }, + { + "id": "app.plugin.manifest.app_error", + "translation": "Ei löydetty manifestia puretusta lisäosasta" + }, + { + "id": "app.plugin.invalid_id.app_error", + "translation": "Lisäosan ID:ssä oltava vähintään {{.Min}} merkkiä, enintään {{.Max}} merkkiä, ja täsmättävä {{.Regex}}." + }, + { + "id": "app.plugin.install_id_failed_remove.app_error", + "translation": "Ei voida asentaa lisäosaa. Lisäosa jolla on sama ID on jo asennettu eikä voitu poistaa." + }, + { + "id": "app.plugin.install_id.app_error", + "translation": "Ei voida asentaa lisäosaa. Lisäosa jolla on sama ID on jo asennettu." + }, + { + "id": "app.plugin.install.app_error", + "translation": "Ei voida asentaa lisäosaa." + }, + { + "id": "app.plugin.get_statuses.app_error", + "translation": "Ei saada lisäosien tilatietoja" + }, + { + "id": "app.plugin.get_plugins.app_error", + "translation": "Ei saada aktiivisa lisäosia" + }, + { + "id": "app.plugin.get_cluster_plugin_statuses.app_error", + "translation": "Ei saada lisäosien tilatietoja klusterista." + }, + { + "id": "app.plugin.filesystem.app_error", + "translation": "Tiedostojärjestelmän virhe" + }, + { + "id": "app.plugin.disabled.app_error", + "translation": "Lisäosat on kytketty pois. Tarkista lokista lisätiedot." + }, + { + "id": "app.plugin.delete_public_key.delete.app_error", + "translation": "Tapahtui virhe poistettaessa OAuth2 appia." + }, + { + "id": "app.plugin.deactivate.app_error", + "translation": "Ei voida kytkeä lisäosaa pois" + }, + { + "id": "app.plugin.config.app_error", + "translation": "Virhe tallennettaessa lisäosan tilaa asetuksiin" + }, + { + "id": "app.plugin.cluster.save_config.app_error", + "translation": "Lisäosan asetukset tiedostossa config.json pitää päivittää käsin kun ReadOnlyConfig klusteroinnin kanssa on käytössä." + }, + { + "id": "app.notification.subject.notification.full", + "translation": "[{{ .SiteName }}] Ilmoitus tiimissä {{ .TeamName}} päivänä {{.Day}} {{.Month}}, {{.Year}}" + }, + { + "id": "app.notification.subject.group_message.generic", + "translation": "[{{ .SiteName }}] Uusi ryhmäviesti {{.Day}} {{.Month}}, {{.Year}}" + }, + { + "id": "app.notification.subject.group_message.full", + "translation": "[{{ .SiteName }}] Uusi ryhmäviesti kanavalla {{ .ChannelName}}, {{.Day}} {{.Month}}, {{.Year}}" + }, + { + "id": "app.notification.subject.direct.full", + "translation": "[{{.SiteName}}] Uusi yksityisviesti lähettäjältä {{.SenderDisplayName}} päivänä {{.Day}} {{.Month}}, {{.Year}}" + }, + { + "id": "api.slackimport.slack_add_users.merge_existing", + "translation": "Slack-käyttäjä on yhdistetty olemassaolevaan Mattermost-käyttäjään jolla sama sähköpostiosoite {{.Email}} ja käyttäjänimi {{.Username}}.\r\n" + }, + { + "id": "api.slackimport.slack_add_users.email_pwd", + "translation": "Slack-käyttäjä jolla sähköposti {{.Email}} ja salasana {{.Password}} on tuotu.\r\n" + }, + { + "id": "api.slackimport.slack_add_users.created", + "translation": "\r\nLuodut käyttäjät:\r\n" + }, + { + "id": "api.slackimport.slack_add_channels.merge", + "translation": "Slack-kanava {{.DisplayName}} on jo olemassa aktiivisena Mattermost-kanavana. Molemmat kanavat on yhdistetty.\r\n" + }, + { + "id": "api.slackimport.slack_add_channels.import_failed", + "translation": "Ei voitu tuoda Slack-kanavaa {{.DisplayName}}.\r\n" + }, + { + "id": "api.slackimport.slack_add_channels.failed_to_add_user", + "translation": "Ei voitu lisätä Slack-käyttäjää {{.Username}} kanavalle.\r\n" + }, + { + "id": "api.slackimport.slack_add_channels.added", + "translation": "\r\nKanavat lisätty:\r\n" + }, + { + "id": "api.slackimport.slack_add_bot_user.unable_import", + "translation": "Ei voitu tuoda Integraatio/Slack Bot käyttäjää {{.Username}}.\r\n" + }, + { + "id": "api.slackimport.slack_add_bot_user.email_pwd", + "translation": "Integraatio/Slack Bot käyttäjä jolla sähköpostiosoite {{.Email}} ja salasana {{.Password}} on tuotu.\r\n" + }, + { + "id": "api.server.start_server.starting.critical", + "translation": "Virhe palvelimen käynnistyksessä, err:%v" + }, + { + "id": "api.server.start_server.rate_limiting_rate_limiter", + "translation": "Ei voitu alustaa käyttömäärän rajoitusta." + }, + { + "id": "api.server.start_server.rate_limiting_memory_store", + "translation": "Ei voitu alustaa käyttömäärää rajoittavaa muistivarastoa. Tarkista MemoryStoreSize asetus." + }, + { + "id": "api.server.start_server.forward80to443.enabled_but_listening_on_wrong_port", + "translation": "Ei voitu ohjata porttia 80 porttiin 443 kuunneltaessa porttia %s: poista Forward80To443 käytettäessä proxy-palvelinta" + }, + { + "id": "api.server.start_server.forward80to443.disabled_while_using_lets_encrypt", + "translation": "Forward80To443 on aktivoitava käytettäessä LetsEncryptiä" + }, + { + "id": "api.scheme.patch_scheme.license.error", + "translation": "Lisenssisi ei tue oikeusjärjestelyjen muuttamista" + }, + { + "id": "api.scheme.get_teams_for_scheme.scope.error", + "translation": "Ei voitu hakea tiimejä järjestelylle koska annettu järjestely ei ole tiimijärjestely." + }, + { + "id": "api.scheme.get_channels_for_scheme.scope.error", + "translation": "Ei voitu hakea kanavia järjestelylle koska annettu järjestely ei ole kanavajärjestely." + }, + { + "id": "api.scheme.delete_scheme.license.error", + "translation": "Lisenssisi ei tue oikeusjärjestelyjen poistoa" + }, + { + "id": "api.scheme.create_scheme.license.error", + "translation": "Lisenssisi ei mahdollista oikeusjärjestelyjen luontia." + }, + { + "id": "api.roles.patch_roles.license.error", + "translation": "Lisenssisi ei tue kehittynyttä oikeushallintaa." + }, + { + "id": "api.restricted_system_admin", + "translation": "Tämä toiminto ei ole sallittu rajoitetulle ylläpitäjälle." + }, + { + "id": "api.reaction.save_reaction.user_id.app_error", + "translation": "Et voi tallentaa reaktiota toiselle käyttäjälle." + }, + { + "id": "api.reaction.save_reaction.invalid.app_error", + "translation": "Reaktio on virheellinen." + }, + { + "id": "api.reaction.save.archived_channel.app_error", + "translation": "Et voi lisätä reaktiota arkistoidulla kanavalla." + }, + { + "id": "api.reaction.delete.archived_channel.app_error", + "translation": "Et voi poistaa reaktiota arkistoidulta kanavalta." + }, + { + "id": "api.push_notification.disabled.app_error", + "translation": "Push-ilmoitukset eivät ole käytössä tällä palvelimella." + }, + { + "id": "api.preference.update_preferences.set.app_error", + "translation": "Käyttäjän asetuksien asettaminen epäonnistui." + }, + { + "id": "api.preference.preferences_category.get.app_error", + "translation": "Käyttän asetuksien haku epäonnistui." + }, + { + "id": "api.preference.delete_preferences.delete.app_error", + "translation": "Käyttäjän asetuksien poisto epäonnistui." + }, + { + "id": "api.post_get_post_by_id.get.app_error", + "translation": "Viestin haku epäonnistui" + }, + { + "id": "api.post.update_post.system_message.app_error", + "translation": "Järjestelmäviestin päivitys ei mahdollista" + }, + { + "id": "api.post.update_post.permissions_time_limit.app_error", + "translation": "Viestin muokkaus on sallittua vain {{.timeLimit}} sekunnin aikana. Pyydä järjestelmänvalvojalta lisätietoja." + }, + { + "id": "api.post.update_post.permissions_details.app_error", + "translation": "Id={{.PostId}} on jo poistettu." + }, + { + "id": "api.post.update_post.find.app_error", + "translation": "Ei löydetty viestiä tai kommentia päivitettäväksi." + }, + { + "id": "api.post.update_post.can_not_update_post_in_deleted.error", + "translation": "Viestiä ei voida päivittää poistetulla kanavalla." + }, + { + "id": "api.post.send_notifications_and_forget.push_message", + "translation": "lähetti sinulle viestin." + }, + { + "id": "api.post.send_notifications_and_forget.push_image_only", + "translation": " liitti tiedoston." + }, + { + "id": "api.post.send_notifications_and_forget.push_general_message", + "translation": " kirjoitti viestin." + }, + { + "id": "api.post.send_notifications_and_forget.push_explicit_mention", + "translation": " mainitsi sinut." + }, + { + "id": "api.post.send_notification_and_forget.push_comment_on_thread", + "translation": " kommentoi ketjussa johon osallistuit." + }, + { + "id": "api.post.send_notification_and_forget.push_comment_on_post", + "translation": " kommentoi viestiäsi." + }, + { + "id": "api.post.send_notification_and_forget.push_channel_mention", + "translation": " ilmoitti kanavalle." + }, + { + "id": "api.post.patch_post.can_not_update_post_in_deleted.error", + "translation": "Viestiä ei voida päivittää poistetulla kanavalla." + }, + { + "id": "api.post.link_preview_disabled.app_error", + "translation": "Järjestelmänvalvoja on poistanut linkkien esikatselun." + }, + { + "id": "api.post.get_message_for_notification.images_sent", + "translation": { + "one": "{{.Count}} kuva lähetetty: {{.Filenames}}", + "other": "{{.Count}} kuvaa lähetetty: {{.Filenames}}" + } + }, + { + "id": "api.post.get_message_for_notification.files_sent", + "translation": { + "one": "{{.Count}} kuva lähetetty: {{.Filenames}}", + "other": "{{.Count}} kuvaa lähetetty: {{.Filenames}}" + } + }, + { + "id": "api.post.do_action.action_integration.app_error", + "translation": "Toiminnon integraatiovirhe" + }, + { + "id": "api.post.do_action.action_id.app_error", + "translation": "Virheellinen action id" + }, + { + "id": "api.post.disabled_here", + "translation": "@here on poistettu käytöstä koska kanavalla on yli {{.Users}} käyttäjää." + }, + { + "id": "api.post.disabled_channel", + "translation": "@channel on poistettu käytöstä koska kanavalla on yli {{.Users}} käyttäjää." + }, + { + "id": "api.post.disabled_all", + "translation": "@all on poistettu käytöstä koska kanavalla on yli {{.Users}} käyttäjää." + }, + { + "id": "api.post.delete_post.can_not_delete_post_in_deleted.error", + "translation": "Ei voida poistaa viestiä poistetulta kanavalta." + }, + { + "id": "api.post.deduplicate_create_post.pending", + "translation": "Hylättiin postaus koska toinen asiakas on tekemässä samaa pyyntöä." + }, + { + "id": "api.post.deduplicate_create_post.failed_to_get", + "translation": "Alkuperäisen viestin noutaminen epäonnistui sen jälkeen kun samaa pyyntöä toistava asiakas oli deduplikoitu." + }, + { + "id": "api.post.create_webhook_post.creating.app_error", + "translation": "Virhe luotaessa viestiä" + }, + { + "id": "api.post.create_post.root_id.app_error", + "translation": "Virheellinen RootId-parametri" + }, + { + "id": "api.post.create_post.channel_root_id.app_error", + "translation": "Virheellinen ChannelId RootId-parametrille" + }, + { + "id": "api.post.create_post.can_not_post_to_deleted.error", + "translation": "Poistetulle kanavalle ei voi kirjoittaa." + }, + { + "id": "api.plugin.upload.no_file.app_error", + "translation": "Puuttuva tiedosto multipart/form pyynnössä" + }, + { + "id": "api.plugin.upload.file.app_error", + "translation": "Ei voitu avata tiedostoa multipart/form pyynnössä" + }, + { + "id": "api.plugin.upload.array.app_error", + "translation": "Tiedostotaulukko on tyhjä multipart/form pyynnössä" + }, + { + "id": "api.plugin.install.download_failed.app_error", + "translation": "Tapahtui virhe ladattaessa liitännäistä." + }, + { + "id": "api.outgoing_webhook.disabled.app_error", + "translation": "Järjestelmänvalvoja on estänyt ulos suunnatut webhookit." + }, + { + "id": "api.oauth.singup_with_oauth.invalid_link.app_error", + "translation": "Liittymislinkki ei ole kelvollinen" + }, + { + "id": "api.oauth.singup_with_oauth.expired_link.app_error", + "translation": "Liittymislinkki on vanhentunut" + }, + { + "id": "api.oauth.singup_with_oauth.disabled.app_error", + "translation": "Käyttäjän liittyminen on pois käytöstä." + }, + { + "id": "api.oauth.revoke_access_token.get.app_error", + "translation": "Virhe noudettaessa access token tietokannasta ennen poistoa." + }, + { + "id": "api.oauth.revoke_access_token.del_token.app_error", + "translation": "Virhe poistettaessa access tokenia tietokannasta." + }, + { + "id": "api.oauth.revoke_access_token.del_session.app_error", + "translation": "Virhe poistettaessa istuntoa tietokannasta." + }, + { + "id": "api.oauth.register_oauth_app.turn_off.app_error", + "translation": "Järjestelmänvalvoja on sammuttanut OAuth2 Service Providerin." + }, + { + "id": "api.oauth.invalid_state_token.app_error", + "translation": "Epäkelpo state token" + }, + { + "id": "api.oauth.get_access_token.refresh_token.app_error", + "translation": "invalid_grant: Epäkelvollinen refresh token" + }, + { + "id": "api.oauth.get_access_token.redirect_uri.app_error", + "translation": "invalid_request: Annettu redirect_uri ei vastaa valtuutuskoodin redirect_uri arvoa" + }, + { + "id": "api.oauth.get_access_token.missing_refresh_token.app_error", + "translation": "invalid_request: Puuttuva refresh_token" + }, + { + "id": "api.oauth.get_access_token.missing_code.app_error", + "translation": "invalid_request: Puuttuva code" + }, + { + "id": "api.oauth.get_access_token.internal_user.app_error", + "translation": "server_error: Palvelimen sisäinen virhe noudettaessa käyttäjää tietokannasta" + }, + { + "id": "api.oauth.get_access_token.internal_session.app_error", + "translation": "server_error: Palvelimen sisäinen virhe tallennettaessa sessiota tietokantaan" + }, + { + "id": "api.oauth.get_access_token.internal_saving.app_error", + "translation": "server_error: Palvelimen sisäinen virhe tallennettaessa access tokenia tietokantaan" + }, + { + "id": "api.oauth.get_access_token.internal.app_error", + "translation": "server_error: Palvelimen sisäinen virhe tietokantayhteydessä" + }, + { + "id": "api.oauth.get_access_token.expired_code.app_error", + "translation": "invalid_grant: Epäkelpo tai vanhentunut valtuutuskoodi" + }, + { + "id": "api.oauth.get_access_token.disabled.app_error", + "translation": "Järjestelmänvalvoja on sammuttanut OAuth2 Service Providerin." + }, + { + "id": "api.oauth.get_access_token.credentials.app_error", + "translation": "invalid_client: Epäkelvot asiakastunnisteet" + }, + { + "id": "api.oauth.get_access_token.bad_grant.app_error", + "translation": "invalid_request: Epäkelpo grant_type" + }, + { + "id": "api.oauth.get_access_token.bad_client_secret.app_error", + "translation": "invalid_request: Puuttuva client_secret" + }, + { + "id": "api.oauth.get_access_token.bad_client_id.app_error", + "translation": "invalid_request: Epäkelpo client_id" + }, + { + "id": "api.oauth.authorize_oauth.disabled.app_error", + "translation": "Järjestelmänvalvoja on sammuttanut OAuth2 Service Providerin." + }, + { + "id": "api.oauth.allow_oauth.turn_off.app_error", + "translation": "Järjestelmänvalvoja on sammuttanut OAuth2 Service Providerin." + }, + { + "id": "api.oauth.allow_oauth.redirect_callback.app_error", + "translation": "invalid_request: Annettu redirect_uri ei vastaa rekisteröityä callback_url arvoa" + }, + { + "id": "api.marshal_error", + "translation": "marshal virhe" + }, + { + "id": "api.license.remove_expired_license.failed.error", + "translation": "Ei voitu lähettää tilin deaktivointisähköpostiviestiä" + }, + { + "id": "api.license.client.old_format.app_error", + "translation": "Asiakaslisenssin uutta muota ei vielä tueta. Määritä format=old pyynnössä." + }, + { + "id": "api.license.add_license.unique_users.app_error", + "translation": "Tämä lisenssi tukee vain {{.Users}} käyttäjää, kun järjestelmässäsi on {{.Count}} uniikkia käyttäjää. Uniikit käyttäjät lasketaan sähköpostiosoitteen perusteella. Näet käyttäjämäärät kohdasta Sivuston raportit -> Katso tilastoja." + }, + { + "id": "api.license.add_license.save_active.app_error", + "translation": "Aktiivinen lisenssi-ID ei tallentunut oikein." + }, + { + "id": "api.license.add_license.save.app_error", + "translation": "Lisenssi ei tallentunut oikein." + }, + { + "id": "api.license.add_license.open.app_error", + "translation": "Lisenssitiedostoa ei voitu avata" + }, + { + "id": "api.license.add_license.no_file.app_error", + "translation": "Ei tiedostoa pyynnön kohdassa 'license'" + }, + { + "id": "api.license.add_license.invalid_count.app_error", + "translation": "Uniikkien käyttäjien laskeminen ei onnistu." + }, + { + "id": "api.license.add_license.invalid.app_error", + "translation": "Virheellinen lisenssitiedosto." + }, + { + "id": "api.license.add_license.expired.app_error", + "translation": "Lisenssi on joko vanhentunut tai ei vielä voimassa." + }, + { + "id": "api.license.add_license.array.app_error", + "translation": "Tyhjä taulukko pyynnön kohdassa 'license'" + }, + { + "id": "api.ldap_groups.license_error", + "translation": "lisenssisi ei tue ldap-ryhmiä" + }, + { + "id": "api.ldap_groups.existing_group_name_error", + "translation": "ryhmän jäsen on jo olemassa" + }, + { + "id": "api.ldap_group.not_found", + "translation": "ldap-ryhmää ei löydetty" + }, + { + "id": "api.io_error", + "translation": "input/output virhe" + }, + { + "id": "api.incoming_webhook.invalid_username.app_error", + "translation": "Virheellinen käyttäjänimi." + }, + { + "id": "api.incoming_webhook.disabled.app_error", + "translation": "Pääkäyttäjä on estänyt sisään tulevat webhookit." + }, + { + "id": "api.file.upload_file.too_large_detailed.app_error", + "translation": "Tiedoston {{.Filename}} lataaminen epäonnistui. {{.Length}} tavua on enemmän kuin suurin sallittu koko {{.Limit}} tavua." + }, + { + "id": "api.file.upload_file.storage.app_error", + "translation": "Kuvan lähettäminen epäonnistui. Kuvien tallennuksen asetuksia ei ole tehty." + }, + { + "id": "api.file.upload_file.read_request.app_error", + "translation": "Tiedoston lataaminen epäonnistui. Virhe pyyntödataa luettaessa." + }, + { + "id": "api.file.upload_file.read_form_value.app_error", + "translation": "Tiedoston lataaminen epäonnistui. Virhe luettaessa arvoa {{.Formname}}." + }, + { + "id": "api.file.upload_file.multiple_channel_ids.app_error", + "translation": "Tiedoston lataaminen epäonnistui. Useita ristiriitaisia channel_id arvoja." + }, + { + "id": "api.file.upload_file.large_image_detailed.app_error", + "translation": "{{.Filename}} ulottuvuudet ({{.Width}} x {{.Height}} pixels) ylittävät rajat" + }, + { + "id": "api.file.upload_file.large_image.app_error", + "translation": "Maksimiulottuvuudet ylittävää tiedostoa ei voitu lähettää: {{.Filename}}" + }, + { + "id": "api.file.upload_file.incorrect_number_of_client_ids.app_error", + "translation": "Tiedoston lataaminen epäonnistui. {{.NumClientIds}} client_id arvoa {{.NumFiles}} tiedostolle." + }, + { + "id": "api.file.read_file.reading_local.app_error", + "translation": "Virhe luettaessa paikallisesta tiedostopalvelimesta" + }, + { + "id": "api.file.no_driver.app_error", + "translation": "Tiedostoajuria ei valittuna." + }, + { + "id": "api.file.get_public_link.no_post.app_error", + "translation": "Ei voitu hakea julkista linkkiä tiedostolle. Tiedoston on oltava liitteenä viestissä jonka nykyinen käyttäjä voi lukea." + }, + { + "id": "api.file.get_public_link.disabled.app_error", + "translation": "Julkiset linkit on poistettu käytöstä" + }, + { + "id": "api.file.get_file_thumbnail.no_thumbnail.app_error", + "translation": "Tiedostolla ei ole esikatselukuvaa" + }, + { + "id": "api.file.get_file_preview.no_preview.app_error", + "translation": "Tiedostolla ei ole esikatselukuvaa" + }, + { + "id": "api.file.get_file.public_invalid.app_error", + "translation": "Julkinen linkki ei näytä olevan kelvollinen" + }, + { + "id": "api.file.attachments.disabled.app_error", + "translation": "Tiedostoliitteet on poistettu käytöstä tällä palvelimella." + }, + { + "id": "api.emoji.upload.open.app_error", + "translation": "Ei voitu luoda emojia. Liitettyä kuvaa avattaessa tapahtui virhe." + }, + { + "id": "api.emoji.upload.large_image.too_large.app_error", + "translation": "Ei voitu luoda emojia. Kuvan on oltava pienempi kuin {{.MaxWidth}} x {{.MaxHeight}}." + }, + { + "id": "api.emoji.upload.large_image.gif_encode_error", + "translation": "Ei voitu luoda emojia. Virhe koodatessa GIF-kuvaa." + }, + { + "id": "api.emoji.upload.large_image.gif_decode_error", + "translation": "Ei voitu luoda emojia. Virhe dekoodatessa GIF-kuvaa." + }, + { + "id": "api.emoji.upload.large_image.encode_error", + "translation": "Ei voitu luoda emojia. Virhe koodatessa kuvaa." + }, + { + "id": "api.emoji.upload.large_image.decode_error", + "translation": "Ei voitu luoda emojia. Virhe dekoodatessa kuvaa." + }, + { + "id": "api.emoji.upload.image.app_error", + "translation": "Hymiötä ei voitu luoda. Tiedoston on oltava PNG, JPEG tai GIF." + }, + { + "id": "api.emoji.storage.app_error", + "translation": "Tiedostojensäilytyspaikan asetukset ovat puutteelliset. Tee joko S3:n tai paikallisen tiedostojensäilytyksen asetukset." + }, + { + "id": "api.emoji.get_image.read.app_error", + "translation": "Ei voitu luke kuvatiedostoa emojille." + }, + { + "id": "api.emoji.get_image.decode.app_error", + "translation": "Ei voitu dekoodata kuvatiedostoa emojille." + }, + { + "id": "api.emoji.disabled.app_error", + "translation": "Pääkäyttäjä on estänyt kustomoitujen emojien käytön." + }, + { + "id": "api.emoji.create.too_large.app_error", + "translation": "Ei voitu luoda emojia. Kuvan pitää olla pienempi kuin 1 Mt." + }, + { + "id": "api.emoji.create.parse.app_error", + "translation": "Ei voitu luoda emojia. Pyynnössä oli jotain vialla." + }, + { + "id": "api.emoji.create.other_user.app_error", + "translation": "Virheellinen käyttäjä-id" + }, + { + "id": "api.emoji.create.duplicate.app_error", + "translation": "Ei voitu luoda emojia. Emoji samalla nimellä on jo olemassa." + }, + { + "id": "api.email_batching.send_batched_email_notification.subject", + "translation": { + "one": "[{{.SiteName}}] uusi ilmoitus {{.Month}} {{.Day}}, {{.Year}}", + "other": "[{{.SiteName}}] uusia ilmoituksia {{.Month}} {{.Day}}, {{.Year}}" + } + }, + { + "id": "api.email_batching.add_notification_email_to_batch.disabled.app_error", + "translation": "Ylläpitäjä on estänyt sähköpostiyhdistelyn" + }, + { + "id": "api.email_batching.add_notification_email_to_batch.channel_full.app_error", + "translation": "Sähköpostiyhdistelyn tulokanava on täynnä. Kasvata asetusta EmailBatchingBufferSize." + }, + { + "id": "api.create_terms_of_service.empty_text.app_error", + "translation": "Kirjoita teksti omiksi Palveluehdoiksi." + }, + { + "id": "api.create_terms_of_service.custom_terms_of_service_disabled.app_error", + "translation": "Omat palveluehdot -toiminto on kytketty pois" + }, + { + "id": "api.context.token_provided.app_error", + "translation": "Istuntosi ei käytä OAuthia, mutta tunniste sisältyi kyselyyn" + }, + { + "id": "api.context.session_expired.app_error", + "translation": "Istuntosi on vanhentunut, kirjaudu uudelleen." + }, + { + "id": "api.context.permissions.app_error", + "translation": "Sinulla ei ole tarvittavia oikeuksia" + }, + { + "id": "api.context.mfa_required.app_error", + "translation": "Monivaiheinen tunnistus on pakollinen tällä palvelimella." + }, + { + "id": "api.context.invalid_url_param.app_error", + "translation": "Virheellinen tai puuttuva {{.Name}} parametri URLissa" + }, + { + "id": "api.context.invalid_token.error", + "translation": "Virheellinen session avain={{.Token}}, err={{.Error}}" + }, + { + "id": "api.context.invalid_param.app_error", + "translation": "Virheellinen {{.Name}} parametri" + }, + { + "id": "api.context.invalid_body_param.app_error", + "translation": "Virheellinen tai puuttuva {{.Name}}" + }, + { + "id": "api.context.404.app_error", + "translation": "Sivua ei löytynyt." + }, + { + "id": "api.config.client.old_format.app_error", + "translation": "Asiakkaan konfiguroinnin uutta muotoa ei tueta vielä. Määritä format=old pyynnössä." + }, + { + "id": "api.command_shrug.name", + "translation": "olankohtautus" + }, + { + "id": "api.command_shrug.hint", + "translation": "[viesti]" + }, + { + "id": "api.command_shrug.desc", + "translation": "Lisää ¯\\_(ツ)_/¯ viestiisi" + }, + { + "id": "api.command_shortcuts.unsupported.app_error", + "translation": "Pikakuvake-toiminto ei ole tuettuna laitteessasi" + }, + { + "id": "api.command_shortcuts.name", + "translation": "oikotiet" + }, + { + "id": "api.command_shortcuts.desc", + "translation": "Näytä näppäinlyhenteet" + }, + { + "id": "api.command_settings.unsupported.app_error", + "translation": "Asetukset-toiminto ei ole tuettu laitteessasi" + }, + { + "id": "api.command_settings.name", + "translation": "asetukset" + }, + { + "id": "api.command_settings.desc", + "translation": "Avaa asetukset" + }, + { + "id": "api.command_search.unsupported.app_error", + "translation": "Etsi-toiminto ei ole tuettu laitteessasi" + }, + { + "id": "api.command_search.name", + "translation": "etsi" + }, + { + "id": "api.command_search.hint", + "translation": "[teksti]" + }, + { + "id": "api.command_search.desc", + "translation": "Etsi tekstiä viesteistä" + }, + { + "id": "api.command_remove.user_not_in_channel", + "translation": "{{.Username}} ei ole tällä kanavalla." + }, + { + "id": "api.command_remove.permission.app_error", + "translation": "Sinulla ei ole oikeutta poistaa jäsentä." + }, + { + "id": "api.command_remove.name", + "translation": "poista" + }, + { + "id": "api.command_remove.missing.app_error", + "translation": "Käyttäjää ei löydetty. Ylläpito on saattanut sulkea käyttäjätunnuksen." + }, + { + "id": "api.command_remove.message.app_error", + "translation": "Viesti pitää toimittaa /remove tai /kick komennolla." + }, + { + "id": "api.command_remove.hint", + "translation": "@[kayttajanimi]" + }, + { + "id": "api.command_remove.direct_group.app_error", + "translation": "Ei voi poistaa ketään suoraviestikanavalta." + }, + { + "id": "api.command_remove.desc", + "translation": "Poista käyttäjä kanavalta" + }, + { + "id": "api.command_open.name", + "translation": "avaa" + }, + { + "id": "api.command_online.success", + "translation": "Olet nyt online-tilassa" + }, + { + "id": "api.command_online.name", + "translation": "verkossa" + }, + { + "id": "api.command_online.desc", + "translation": "Muuta tilaksesi online" + }, + { + "id": "api.command_offline.success", + "translation": "Olet nyt näkymätön" + }, + { + "id": "api.command_offline.name", + "translation": "poissa" + }, + { + "id": "api.command_offline.desc", + "translation": "Muuta tila poissaolevaksi" + }, + { + "id": "api.command_mute.success_unmute_direct_msg", + "translation": "Kanava ei ole enää mykistetty." + }, + { + "id": "api.command_mute.success_unmute", + "translation": "{{.Channel}} ei enää mykistetty." + }, + { + "id": "api.command_mute.success_mute_direct_msg", + "translation": "Et saa ilmoituksia tältä kanavalta kunnes mykistys on otettu pois päältä." + }, + { + "id": "api.command_mute.success_mute", + "translation": "Et saa ilmoituksia kanavalta {{.Channel}} ennen kuin mykistys on otettu pois päältä." + }, + { + "id": "api.command_mute.not_member.error", + "translation": "Ei voitu mykistää kanavaa {{.Channel}} koska et ole sen jäsen." + }, + { + "id": "api.command_mute.no_channel.error", + "translation": "Kanavaa ei löydetty. Käytä [kanavan tunnistetta](https://about.mattermost.com/default-channel-handle-documentation) sen määrittämiseen." + }, + { + "id": "api.command_mute.name", + "translation": "mykistä" + }, + { + "id": "api.command_mute.hint", + "translation": "~[kanava]" + }, + { + "id": "api.command_mute.error", + "translation": "Kanavaa {{.Channel}} ei löydetty. Käytä [kanavan tunnistetta](https://about.mattermost.com/default-channel-handle-documentation) kanavan määrittämiseen." + }, + { + "id": "api.user.login_by_oauth.not_available.app_error", + "translation": "{{.Service}} SSO OAuth 2.0:n kautta ei tarjolla tällä palvelimella" + }, + { + "id": "api.user.login.use_auth_service.app_error", + "translation": "Kirjaudu käyttäen palvelua {{.AuthService}}" + }, + { + "id": "api.user.login.not_verified.app_error", + "translation": "Kirjautuminen epäonnistui koska sähköpostiosoitetta ei ole vahvistettu" + }, + { + "id": "api.user.login.invalid_credentials_username", + "translation": "Syötä kelvollinen käyttäjätunnus ja/tai salasana." + }, + { + "id": "api.user.login.invalid_credentials_email_username", + "translation": "Syötä kelvollinen sähköpostiosoite tai käyttäjätunnus ja/tai salasana." + }, + { + "id": "api.user.login.invalid_credentials_email", + "translation": "Syötä kelvollinen sähköpostiosoite ja/tai salasana" + }, + { + "id": "api.user.login.inactive.app_error", + "translation": "Kirjautuminen epäonnistui, koska tilisi on inaktivoitu. Ota yhteyttä ylläpitäjään." + }, + { + "id": "api.user.login.guest_accounts.license.error", + "translation": "Lisenssisi ei tue vierailijatilejä" + }, + { + "id": "api.user.login.client_side_cert.license.app_error", + "translation": "Yritys käyttää kokeellista ClientSideCertEnable-toimintoa ilman käypää enterprise-lisenssiä" + }, + { + "id": "api.user.login.client_side_cert.certificate.app_error", + "translation": "Yritettiin kirjautua käyttäen kokeellista ClientSideCert-toimintoa ilman käypää sertifikaattia" + }, + { + "id": "api.user.login.blank_pwd.app_error", + "translation": "Salasanakenttä ei saa olla tyhjä" + }, + { + "id": "api.user.ldap_to_email.not_ldap_account.app_error", + "translation": "Tämä käyttäjätili ei käytä AD/LDAP:a" + }, + { + "id": "api.user.ldap_to_email.not_available.app_error", + "translation": "AD/LDAP ei ole saatavilla tällä palvelimella" + }, + { + "id": "api.user.get_user_by_email.permissions.app_error", + "translation": "Käyttäjää ei voitu hakea sähköpostin perusteella." + }, + { + "id": "api.user.email_to_oauth.not_available.app_error", + "translation": "Autentikointisiirtoa ei ole määritetty tai ei ole tarjolla tällä palvelimella." + }, + { + "id": "api.user.email_to_ldap.not_available.app_error", + "translation": "AD/LDAP ei ole saatavilla tällä palvelimella" + }, + { + "id": "api.user.create_user.signup_link_invalid.app_error", + "translation": "Liittymislinkki ei ole kelvollinen" + }, + { + "id": "api.user.create_user.signup_link_expired.app_error", + "translation": "Liittymislinkki on vanhentunut" + }, + { + "id": "api.user.create_user.signup_email_disabled.app_error", + "translation": "Liittyminen käyttäjäksi sähköpostilla on kytketty pois." + }, + { + "id": "api.user.create_user.no_open_server", + "translation": "Tämä palvelin ei salli avoimia liittymisiä. Ota yhteyttä järjestelmänvalvojaan saadaksesi kutsun." + }, + { + "id": "api.user.create_user.guest_accounts.license.app_error", + "translation": "Lisenssisi ei tue vierailijatilejä." + }, + { + "id": "api.user.create_user.disabled.app_error", + "translation": "Käyttäjän luonti on kytketty pois." + }, + { + "id": "api.user.create_user.accepted_domain.app_error", + "translation": "Sähköpostiosoitteesi ei kuulu hyväksyttyyn domainiin. Ota yhteyttä järjestelmänvalvojaan tai liity toisella sähköpostiosoitteella." + }, + { + "id": "api.user.create_profile_image.initial.app_error", + "translation": "Ei voitu lisätä käyttäjän nimikirjainta oletusprofiilikuvaan" + }, + { + "id": "api.user.create_profile_image.encode.app_error", + "translation": "Ei voitu enkoodata oletusprofiilikuvaa" + }, + { + "id": "api.user.create_profile_image.default_font.app_error", + "translation": "Ei voitu luoda oletusprofiilikuvan fonttia" + }, + { + "id": "api.user.create_oauth_user.create.app_error", + "translation": "Käyttäjää ei voitu luoda palvelun {{.Service}} käyttäjäobjektista" + }, + { + "id": "api.user.create_oauth_user.already_attached.app_error", + "translation": "Tili on jo olemassa tällä sähköpostiosoitteella joka käyttää toista kirjautumismenetelmää kuin {{.Service}}. Kirjaudu käyttämällä menetelmää {{.Auth}}." + }, + { + "id": "api.user.create_email_token.error", + "translation": "Ei voitu luoda token dataa sähköpostivahvistukseen" + }, + { + "id": "api.user.complete_switch_with_oauth.parse.app_error", + "translation": "Ei voitu lukea auth dataa {{.Service}} käyttäjäobjektista" + }, + { + "id": "api.user.complete_switch_with_oauth.blank_email.app_error", + "translation": "Tyhjä email" + }, + { + "id": "api.user.check_user_password.invalid.app_error", + "translation": "Kirjautuminen epäonnistui virheellisen salasanan vuoksi" + }, + { + "id": "api.user.check_user_mfa.bad_code.app_error", + "translation": "Virheellinen MFA token." + }, + { + "id": "api.user.check_user_login_attempts.too_many.app_error", + "translation": "Tilisi on lukittu liian monen salasanayrityksen vuoksi. Palauta salasanasi." + }, + { + "id": "api.user.authorize_oauth_user.unsupported.app_error", + "translation": "OAuth-palveluntarjoajaa ei tueta" + }, + { + "id": "api.user.authorize_oauth_user.token_failed.app_error", + "translation": "Token-pyyntö epäonnistui" + }, + { + "id": "api.user.authorize_oauth_user.service.app_error", + "translation": "Token-pyyntö palveluun {{.Service}} epäonnistui" + }, + { + "id": "api.user.authorize_oauth_user.response.app_error", + "translation": "Vastaanotettiin virheellinen vastaus OAuth palveluntarjoajalta" + }, + { + "id": "api.user.authorize_oauth_user.missing.app_error", + "translation": "Puuttuva access token" + }, + { + "id": "api.user.authorize_oauth_user.invalid_state.app_error", + "translation": "Virheellinen state" + }, + { + "id": "api.user.authorize_oauth_user.bad_token.app_error", + "translation": "Virheellinen token-tyyppi" + }, + { + "id": "api.user.authorize_oauth_user.bad_response.app_error", + "translation": "Virheellinen vastaus token-pyyntöön" + }, + { + "id": "api.user.add_direct_channels_and_forget.failed.error", + "translation": "Virhe lisättäessä suorakanavan asetukset käyttäjälle user_id={{.UserId}}, team_id={{.TeamId}}, err={{.Error}}" + }, + { + "id": "api.user.activate_mfa.email_and_ldap_only.app_error", + "translation": "MFA ei ole käytettävissä tällä tilityypillä" + }, + { + "id": "api.templates.welcome_subject", + "translation": "[{{ .SiteName }}] Liityit palveluun {{ .ServerURL }}" + }, + { + "id": "api.templates.welcome_body.title", + "translation": "Olet liittynyt palveluun {{ .ServerURL }}" + }, + { + "id": "api.templates.welcome_body.info", + "translation": "Vahvista sähköpostiosoitteesi klikkaamalla alla." + }, + { + "id": "api.templates.welcome_body.button", + "translation": "Vahvista sähköposti" + }, + { + "id": "api.templates.welcome_body.app_download_info", + "translation": "Parasta kokemusta varten lataa sovellukset PC:lle, Macille, iOS:lle ja Androidille:" + }, + { + "id": "api.templates.verify_subject", + "translation": "[{{ .SiteName }}] Sähköpostin vahvistus" + }, + { + "id": "api.templates.verify_body.title", + "translation": "Liityit palveluun {{ .ServerURL }}" + }, + { + "id": "api.templates.verify_body.info", + "translation": "Vahvista sähköpostiosoitteesi klikkaamalla alla." + }, + { + "id": "api.templates.verify_body.button", + "translation": "Vahvista sähköposti" + }, + { + "id": "api.templates.username_change_subject", + "translation": "[{{ .SiteName }}] Käyttäjänimesi on muutettu" + }, + { + "id": "api.templates.username_change_body.title", + "translation": "Muutit käyttäjänimesi" + }, + { + "id": "api.templates.username_change_body.info", + "translation": "Käyttäjänimi tiimissä {{.TeamDisplayName}} on vaihdettu nimeen {{.NewUsername}}." + }, + { + "id": "api.templates.user_access_token_subject", + "translation": "[{{ .SiteName }}] Henkilökohtainen access token on lisätty tiliisi" + }, + { + "id": "api.templates.user_access_token_body.title", + "translation": "Henkilökohtainen access token on lisätty tiliisi" + }, + { + "id": "api.templates.user_access_token_body.info", + "translation": "Henkilökohtainen access token on lisätty tiliisi osoitteessa {{ .SiteURL }}. Sillä voidaan käyttää palvelua {{.SiteName}} tilisi kanssa." + }, + { + "id": "api.templates.signin_change_email.subject", + "translation": "[{{ .SiteName }}] Kirjautumismenetelmäsi on vaihdettu" + }, + { + "id": "api.templates.signin_change_email.body.title", + "translation": "Muutit kirjautumismenetelmää" + }, + { + "id": "api.templates.signin_change_email.body.method_email", + "translation": "sähköposti ja salasana" + }, + { + "id": "api.templates.signin_change_email.body.info", + "translation": "Muutit kirjautumismenetelmäsi sivustolla {{ .SiteName }} menetelmään {{.Method}}." + }, + { + "id": "api.templates.reset_subject", + "translation": "[{{ .SiteName }}] Palauta salasanasi" + }, + { + "id": "api.templates.reset_body.title", + "translation": "Pyysit salasanan palautusta" + }, + { + "id": "api.command_mute.desc", + "translation": "Estä työpöytä-, sähköposti- ja push-ilmoitukset aktiivisella tai määritetyllä [kanavalla]." + }, + { + "id": "api.command_msg.permission.app_error", + "translation": "Sinulla ei ole oikeuksia lähettää yksityisviestiä tälle käyttäjälle." + }, + { + "id": "api.command_msg.name", + "translation": "viesti" + }, + { + "id": "api.command_msg.missing.app_error", + "translation": "Ei löydetty käyttäjää." + }, + { + "id": "api.command_msg.hint", + "translation": "@[käyttäjätunnus] 'viesti'" + }, + { + "id": "api.command_msg.fail.app_error", + "translation": "Viestin lähettäminen käyttäjälle epäonnistui." + }, + { + "id": "api.command_msg.dm_fail.app_error", + "translation": "Yksityisviestin luonnissa tapahtui virhe." + }, + { + "id": "api.command_msg.desc", + "translation": "Lähetä yksityisviesti käyttäjälle" + }, + { + "id": "api.command_me.name", + "translation": "minä" + }, + { + "id": "api.command_me.hint", + "translation": "[viesti]" + }, + { + "id": "api.command_me.desc", + "translation": "Toiminto" + }, + { + "id": "api.command_logout.name", + "translation": "uloskirjautuminen" + }, + { + "id": "api.command_logout.desc", + "translation": "Uloskirjaus Mattermostista" + }, + { + "id": "api.command_leave.name", + "translation": "poistu" + }, + { + "id": "api.command_leave.fail.app_error", + "translation": "Virhe tapahtui kanavalle poistumisen aikana." + }, + { + "id": "api.command_leave.desc", + "translation": "Poistu nykyiseltä kanavalta" + }, + { + "id": "api.command_kick.name", + "translation": "potkaise" + }, + { + "id": "api.command_join.name", + "translation": "liity" + }, + { + "id": "api.command_join.missing.app_error", + "translation": "Kanavaa ei löydetty" + }, + { + "id": "api.command_join.list.app_error", + "translation": "Virhe tapahtui kanavien listauksen aikana." + }, + { + "id": "api.command_join.hint", + "translation": "~[kanava]" + }, + { + "id": "api.command_join.fail.app_error", + "translation": "Virhe tapahtui kanavalle liittymisen aikana." + }, + { + "id": "api.command_join.desc", + "translation": "Liity avoimelle kanavalle" + }, + { + "id": "api.command_invite_people.permission.app_error", + "translation": "Sinulla ei ole oikeuksia kutsua uusia käyttäjiä tälle palvelimelle." + }, + { + "id": "api.command_invite.user_not_in_team.app_error", + "translation": "@{{.Username}} ei ole tiimin jäsen." + }, + { + "id": "api.command_invite.user_already_in_channel.app_error", + "translation": "{{.User}} on jo kanavalla." + }, + { + "id": "api.command_invite.success", + "translation": "{{.User}} lisättiin kanavalle {{.Channel}}." + }, + { + "id": "api.command_invite.private_channel.app_error", + "translation": "Kanavaa {{.Channel}} ei löydetty. Käytä kanavan tunnistetta sen määrittämiseen." + }, + { + "id": "api.command_invite.permission.app_error", + "translation": "Sinulla ei ole oikeuksia lisätä käyttäjää {{.User}} kanavalle {{.Channel}}." + }, + { + "id": "api.command_invite.name", + "translation": "kutsu" + }, + { + "id": "api.command_invite.missing_user.app_error", + "translation": "Käyttäjää ei ole olemassa. Ylläpito on saattanut sulkea käyttäjätunnuksen." + }, + { + "id": "api.command_invite.missing_message.app_error", + "translation": "Käyttäjänimi ja kanava puuttuvat." + }, + { + "id": "api.command_invite.hint", + "translation": "@[käyttäjätunnus] ~[kanava]" + }, + { + "id": "api.command_invite.fail.app_error", + "translation": "Kanavalle liittymisen yhteydessä tapahtui virhe." + }, + { + "id": "api.command_invite.directchannel.app_error", + "translation": "Et voi lisätä ketään suoraviestikanavalle." + }, + { + "id": "api.command_invite.desc", + "translation": "Kutsu käyttäjä kanavalle" + }, + { + "id": "api.command_invite.channel.error", + "translation": "Kanavaa {{.Channel}} ei löydetty. Käytä [kanavan tunnistetta](https://about.mattermost.com/default-channel-handle-documentation) kanavan löytämiseen." + }, + { + "id": "api.command_invite.channel.app_error", + "translation": "Virhe noudettaessa kanavaa." + }, + { + "id": "api.command_help.name", + "translation": "ohje" + }, + { + "id": "api.command_help.desc", + "translation": "Avaa Mattermostin ohjesivusto" + }, + { + "id": "api.command_groupmsg.permission.app_error", + "translation": "Sinulla ei ole oikeuksia lähettää uutta ryhmäviestiä." + }, + { + "id": "api.command_groupmsg.name", + "translation": "viesti" + }, + { + "id": "api.command_groupmsg.min_users.app_error", + "translation": "Ryhmäviesti on rajoitettu vähintään {{.MinUsers}} käyttäjälle." + }, + { + "id": "api.command_groupmsg.max_users.app_error", + "translation": "Ryhmäviesti on rajoitettu enintään {{.MaxUsers}} käyttäjälle." + }, + { + "id": "api.command_groupmsg.invalid_user.app_error", + "translation": { + "one": "Käyttäjää ei löydy: {{.Users}}", + "other": "Käyttäjiä ei löydy: {{.Users}}" + } + }, + { + "id": "api.command_groupmsg.hint", + "translation": "@[kayttaja1],@[kayttaja2] 'viesti'" + }, + { + "id": "api.command_groupmsg.group_fail.app_error", + "translation": "Ryhmäviestin luonnissa tapahtui virhe." + }, + { + "id": "api.command_groupmsg.fail.app_error", + "translation": "Viestin lähettäminen käyttäjälle epäonnistui." + }, + { + "id": "api.command_groupmsg.desc", + "translation": "Lähetä ryhmäviesti tietyille käyttäjille" + }, + { + "id": "api.command_expand_collapse.fail.app_error", + "translation": "Tapahtui virhe esikatseluiden näyttämisessä" + }, + { + "id": "api.command_expand.success", + "translation": "Kuvien linkit näytetään oletuksena" + }, + { + "id": "api.command_expand.name", + "translation": "laajenna" + }, + { + "id": "api.command_expand.desc", + "translation": "Poista käytöstä kuvien automaattinen esikatselu" + }, + { + "id": "api.command_echo.name", + "translation": "kaiku" + }, + { + "id": "api.command_echo.message.app_error", + "translation": "Viesti pitää toimittaa /echo komennolla." + }, + { + "id": "api.command_echo.hint", + "translation": "'viesti' [viive sekunneissa]" + }, + { + "id": "api.command_echo.high_volume.app_error", + "translation": "Iso määrä kopiointipyyntöjä, ei voida suorittaa" + }, + { + "id": "api.command_echo.desc", + "translation": "Kopioi teksti takaisin tililtäsi" + }, + { + "id": "api.command_echo.delay.app_error", + "translation": "Viiveen on oltava alta 10000 sekuntia" + }, + { + "id": "api.command_dnd.success", + "translation": "Älä häiritse on päällä. Et vastaanota työpöytä- tai mobiilisovelluksen push-viestejä kunnes Älä häiritse on otettu pois." + }, + { + "id": "api.command_dnd.name", + "translation": "älä häiritse" + }, + { + "id": "api.command_dnd.desc", + "translation": "Älä häiritse poistaa työpöytä- ja mobiiliohjelmien push-viestit." + }, + { + "id": "api.command_collapse.success", + "translation": "Kuvien linkit avataan oletuksena" + }, + { + "id": "api.command_collapse.name", + "translation": "tiivistä" + }, + { + "id": "api.command_collapse.desc", + "translation": "Laita kuvien esikatselun automaattikatselu päälle" + }, + { + "id": "api.command_code.name", + "translation": "koodi" + }, + { + "id": "api.command_code.message.app_error", + "translation": "Viesti pitää toimittaa /code komennon avulla." + }, + { + "id": "api.command_code.hint", + "translation": "[teksti]" + }, + { + "id": "api.command_code.desc", + "translation": "Näytä teksti koodiblokkina" + }, + { + "id": "api.command_channel_rename.update_channel.app_error", + "translation": "Virhe päivittäessä kanavaa." + }, + { + "id": "api.command_channel_rename.too_short.app_error", + "translation": "Kanavan nimen pitää olla {{.Length}} merkkiä pitkä tai pidempi" + }, + { + "id": "api.command_channel_rename.too_long.app_error", + "translation": "Kanavan nimi pitää olla {{.Length}} merkkiä pitkä tai lyhyempi" + }, + { + "id": "api.command_channel_rename.permission.app_error", + "translation": "Sinulla ei ole oikeuksia uudelleennimetä kanavaa." + }, + { + "id": "api.command_channel_rename.name", + "translation": "nimeä uudelleen" + }, + { + "id": "api.command_channel_rename.message.app_error", + "translation": "Viesti pitää olla /rename komennon mukana." + }, + { + "id": "api.command_channel_rename.hint", + "translation": "[teksti]" + }, + { + "id": "api.command_channel_rename.direct_group.app_error", + "translation": "Et voi uudelleennimetä suoraviestikanavaa." + }, + { + "id": "api.command_channel_rename.desc", + "translation": "Nimeä kanava uudelleen" + }, + { + "id": "api.command_channel_rename.channel.app_error", + "translation": "Virhe avatessa kanavaa." + }, + { + "id": "api.command_channel_remove.channel.app_error", + "translation": "Virhe noudettaessa kanavaa." + }, + { + "id": "api.command_channel_purpose.update_channel.app_error", + "translation": "Virhe päivittäessä kanavaa." + }, + { + "id": "api.command_channel_purpose.permission.app_error", + "translation": "Sinulla ei ole oikeuksia muokata kanavan tarkoitusta." + }, + { + "id": "api.command_channel_purpose.name", + "translation": "tarkoitus" + }, + { + "id": "api.command_channel_purpose.message.app_error", + "translation": "Viesti pitää lähettää /purpose komennolla." + }, + { + "id": "api.command_channel_purpose.hint", + "translation": "[teksti]" + }, + { + "id": "api.command_channel_purpose.direct_group.app_error", + "translation": "Ei voida määritellä tarkoitusta suoraviestikanavalle. Käytä /header asettaaksesi otsikko." + }, + { + "id": "api.command_channel_purpose.desc", + "translation": "Muokkaa kanavan tarkoitusta" + }, + { + "id": "api.command_channel_purpose.channel.app_error", + "translation": "Virhe avatessa kanavaa." + }, + { + "id": "api.command_channel_header.update_channel.app_error", + "translation": "Virhe päivittäessä kanavaa." + }, + { + "id": "api.command_channel_header.permission.app_error", + "translation": "Sinulla ei ole oikeuksia muokata kanavan otsikkoa." + }, + { + "id": "api.command_channel_header.name", + "translation": "ylätunniste" + }, + { + "id": "api.command_channel_header.message.app_error", + "translation": "Teksti pitää kirjoittaa /header komennolla." + }, + { + "id": "api.command_channel_header.hint", + "translation": "[teksti]" + }, + { + "id": "api.command_channel_header.desc", + "translation": "Muokkaa kanavan otsikkoa" + }, + { + "id": "api.command_channel_header.channel.app_error", + "translation": "Virhe noudettaessa kanavaa." + }, + { + "id": "api.command_away.success", + "translation": "Olet nyt poissa" + }, + { + "id": "api.command_away.name", + "translation": "poissaoleva" + }, + { + "id": "api.command_away.desc", + "translation": "Aseta tilasi poissaolevaksi" + }, + { + "id": "api.command.team_mismatch.app_error", + "translation": "Ei voi päivittää komentoja tiimien välillä" + }, + { + "id": "api.command.invite_people.sent", + "translation": "Sähköpostikutsut lähetettiin" + }, + { + "id": "api.command.invite_people.no_email", + "translation": "Anna yksi tai useampi oikeellinen sähköpostiosoite" + }, + { + "id": "api.command.invite_people.name", + "translation": "kutsu_ihmisiä" + }, + { + "id": "api.command.invite_people.invite_off", + "translation": "Käyttäjien luonti on poistettu käytöstä, kutsuja ei lähetetty" + }, + { + "id": "api.command.invite_people.hint", + "translation": "[nimi@osoite.com ...]" + }, + { + "id": "api.command.invite_people.fail", + "translation": "Kutsuja lähetettäessä tapahtui virhe" + }, + { + "id": "api.command.invite_people.email_off", + "translation": "Sähköpostia ei ole määritetty, kutsuja ei lähetetty" + }, + { + "id": "api.command.invite_people.email_invitations_off", + "translation": "Sähköpostikutsut kytketty pois, kutsuja ei lähetetty" + }, + { + "id": "api.command.invite_people.desc", + "translation": "Kutsu käyttäjiä Mattermost-ryhmääsi" + }, + { + "id": "api.command.execute_command.start.app_error", + "translation": "Komentia ei löydy" + }, + { + "id": "api.command.execute_command.not_found.app_error", + "translation": "Komentoa '{{.Trigger}}' ei löytynyt. Lähettääksesi viestin \"/\" -merkillä, lisää tyhjä välilyönti viestin alkuun." + }, + { + "id": "api.command.execute_command.failed_resp.app_error", + "translation": "Komento '{{.Trigger}}' palautti vastauksen {{.Status}}" + }, + { + "id": "api.command.execute_command.failed_empty.app_error", + "translation": "Komento '{{.Trigger}}' palautti tyhjän vastauksen" + }, + { + "id": "api.command.execute_command.failed.app_error", + "translation": "Komento '{{.Trigger}}' epäonnistui" + }, + { + "id": "api.command.execute_command.create_post_failed.app_error", + "translation": "Komento '{{.Trigger}}' ei postannut vastausta. Ota yhteys järjestelmänvalvojaan." + }, + { + "id": "api.command.duplicate_trigger.app_error", + "translation": "Tämä laukaisusana on jo käytössä. Valitse toinen sana." + }, + { + "id": "api.command.disabled.app_error", + "translation": "Pääkäyttäjä on estänyt komentojen käyttämisen." + }, + { + "id": "api.command.command_post.forbidden.app_error", + "translation": "Valittu käyttäjä ei ole valitun kanavan jäsen." + }, + { + "id": "api.command.admin_only.app_error", + "translation": "Vain pääkäyttäjät voivat luoda rajapintaintegraatioita." + }, + { + "id": "api.channel.update_team_member_roles.scheme_role.app_error", + "translation": "Järjestely hallinnoi annettua roolia eikä sitä siksi voida antaa suoraan tiimin jäsenelle." + }, + { + "id": "api.channel.update_channel_scheme.scheme_scope.error", + "translation": "Järjestelyä ei voida asettaa kanavalle koska annettu järjestely ei ole kanavajärjestely." + }, + { + "id": "api.channel.update_channel_scheme.license.error", + "translation": "Lisenssisi ei mahdollista kanavan järjestelyn muuttamista" + }, + { + "id": "api.channel.update_channel_member_roles.scheme_role.app_error", + "translation": "Järjestely hallinnoi annettua roolia eikä sitä siksi voida määrittää suoraan kanavan jäsenelle." + }, + { + "id": "api.channel.update_channel.tried.app_error", + "translation": "Yritetty suorittaa virheellinen päivitys oletuskanavalle {{.Channel}}." + }, + { + "id": "api.channel.update_channel.deleted.app_error", + "translation": "Kanava on arkistoitu tai poistettu." + }, + { + "id": "api.channel.rename_channel.cant_rename_group_messages.app_error", + "translation": "Et voi uudelleennimetä ryhmäviestikanavaa." + }, + { + "id": "api.channel.rename_channel.cant_rename_direct_messages.app_error", + "translation": "Et voi uudelleennimetä suoraviestikanavaa." + }, + { + "id": "api.channel.remove_user_from_channel.app_error", + "translation": "Käyttäjää ei voi poistaa tämän tyyppiseltä kanavalta." + }, + { + "id": "api.channel.remove_member.removed", + "translation": "%v poistettiin kanavalta." + }, + { + "id": "api.channel.remove_channel_member.type.app_error", + "translation": "Käyttäjän poistaminen kanavalta ei onnistunut." + }, + { + "id": "api.channel.remove.default.app_error", + "translation": "Käyttäjää ei voida poistaa oletuskanavalta {{.Channel}}." + }, + { + "id": "api.channel.post_user_add_remove_message_and_forget.error", + "translation": "Liity/poistu-viestin lähettäminen epäonnistui" + }, + { + "id": "api.channel.post_update_channel_header_message_and_forget.updated_to", + "translation": "%s päivitti kanavaotsikoksi %s" + }, + { + "id": "api.channel.post_update_channel_header_message_and_forget.updated_from", + "translation": "%s päivitti kanavaotsikon %s, uusi kanavaotsikko: %s" + }, + { + "id": "api.channel.post_update_channel_header_message_and_forget.retrieve_user.error", + "translation": "Käyttäjän hakeminen kanavaotsikon päivittämisen aikana epäonnistui" + }, + { + "id": "api.channel.post_update_channel_header_message_and_forget.removed", + "translation": "%s poisti kanavaotsikon (oli: %s)" + }, + { + "id": "api.channel.post_update_channel_header_message_and_forget.post.error", + "translation": "Virhe päivittäessä kanavan header-viestiä" + }, + { + "id": "api.channel.post_update_channel_displayname_message_and_forget.updated_from", + "translation": "%s päivitti kanavaotsikon %s, uusi kanavaotsikko: %s" + }, + { + "id": "api.channel.post_update_channel_displayname_message_and_forget.retrieve_user.error", + "translation": "Käyttäjän hakeminen kanavaotsikon päivittämisen aikana epäonnistui" + }, + { + "id": "api.channel.post_update_channel_displayname_message_and_forget.create_post.error", + "translation": "Näyttönimen päivitysviestin lähettäminen epäonnistui" + }, + { + "id": "api.channel.post_channel_privacy_message.error", + "translation": "Kanavan yksityisyyspäivitysviestin asettaminen epäonnistui." + }, + { + "id": "api.channel.patch_update_channel.forbidden.app_error", + "translation": "Kanavan päivitys epäonnistui." + }, + { + "id": "api.channel.patch_channel_moderations.license.error", + "translation": "Lisenssisi ei tue kanavan moderointia." + }, + { + "id": "api.channel.leave.left", + "translation": "%v poistui kanavalta." + }, + { + "id": "api.channel.leave.last_member.app_error", + "translation": "Olet viimeinen käyttäjä jäljellä, yritä poistaa yksityiskanava pois lähtemisen sijasta." + }, + { + "id": "api.channel.leave.direct.app_error", + "translation": "Et voi poistua suoraviestikanavalta." + }, + { + "id": "api.channel.leave.default.app_error", + "translation": "Oletuskanavalta {{.Channel}} poistuminen ei ole mahdollista." + }, + { + "id": "api.channel.join_channel.post_and_forget", + "translation": "%v liittyi kanavalle." + }, + { + "id": "api.channel.join_channel.permissions.app_error", + "translation": "Sinulla ei ole tarvittavia oikeuksia." + }, + { + "id": "api.channel.get_channel_moderations.license.error", + "translation": "Lisenssisi ei tue kanavan moderointia." + }, + { + "id": "api.channel.delete_channel.type.invalid", + "translation": "Suora- tai ryhmäviestikanavien poistaminen ei ole mahdollista" + }, + { + "id": "api.channel.delete_channel.deleted.app_error", + "translation": "Kanava on arkistoitu tai poistettu." + }, + { + "id": "api.channel.delete_channel.cannot.app_error", + "translation": "Ei voida poistaa oletuskanavaa {{.Channel}}." + }, + { + "id": "api.channel.delete_channel.archived", + "translation": "%v on arkistoinut kanavan." + }, + { + "id": "api.channel.create_group.bad_user.app_error", + "translation": "Yksi syötetyistä käyttäjistä ei ole olemassa." + }, + { + "id": "api.channel.create_group.bad_size.app_error", + "translation": "Ryhmäviestikanavilla oltava vähintään 3 ja enintään 8 käyttäjää." + }, + { + "id": "api.channel.create_direct_channel.invalid_user.app_error", + "translation": "Virheellinen käyttäjä-ID suoraviestikanavan luontia varten." + }, + { + "id": "api.channel.create_default_channels.town_square", + "translation": "Kaupungin tori" + }, + { + "id": "api.channel.create_default_channels.off_topic", + "translation": "Asiaan kuulumaton" + }, + { + "id": "api.channel.create_channel.max_channel_limit.app_error", + "translation": "Useamman kuin {{.MaxChannelsPerTeam}} kanavan luominen nykyiselle tiimille ei onnistu." + }, + { + "id": "api.channel.create_channel.direct_channel.app_error", + "translation": "Käytettävä createDirectChannel API-palvelua suoraviestikanavan luontiin." + }, + { + "id": "api.channel.channel_member_counts_by_group.license.error", + "translation": "lisenssisi ei tue ldap-ryhmiä" + }, + { + "id": "api.channel.change_channel_privacy.public_to_private", + "translation": "Kanava muutettiin yksityiseksi." + }, + { + "id": "api.channel.change_channel_privacy.private_to_public", + "translation": "Kanava muuttettiin julkiseksi ja kuka tahansa voi liittyä sille." + }, + { + "id": "api.channel.add_user_to_channel.type.app_error", + "translation": "Käyttäjää ei voida lisätä tämän tyyppiselle kanavalle." + }, + { + "id": "api.channel.add_user.to.channel.failed.deleted.app_error", + "translation": "Käyttäjän lisäys kanavalle epäonnistui, koska käyttäjä ei ole tiimin jäsen." + }, + { + "id": "api.channel.add_user.to.channel.failed.app_error", + "translation": "Käyttäjän lisääminen kanavalle epäonnistui." + }, + { + "id": "api.channel.add_members.error", + "translation": "Virhe jäsen(i)en lisäämisessä kanavalle." + }, + { + "id": "api.channel.add_member.added", + "translation": "%v lisättiin kanavalle, lisääjänä %v." + }, + { + "id": "api.channel.add_guest.added", + "translation": "%v lisättiin kanavalle, lisääjänä %v." + }, + { + "id": "api.bot.create_disabled", + "translation": "Botin luonti on poistetty käytöstä." + }, + { + "id": "api.admin.upload_brand_image.too_large.app_error", + "translation": "Tiedoston lataaminen epäonnistui. Tiedosto on liian suuri." + }, + { + "id": "api.admin.upload_brand_image.storage.app_error", + "translation": "Kuvan lataaminen epäonnistui. Kuvien tallennuksen asetuksia ei ole tehty." + }, + { + "id": "api.admin.upload_brand_image.parse.app_error", + "translation": "Moniosaista lomaketta ei voitu jäsentää." + }, + { + "id": "api.admin.upload_brand_image.no_file.app_error", + "translation": "Ei tiedostoa pyynnön kohdassa 'image'." + }, + { + "id": "api.admin.upload_brand_image.array.app_error", + "translation": "Tyhjä taulukko pyynnön kohdassa 'image'." + }, + { + "id": "api.admin.test_s3.missing_s3_bucket", + "translation": "S3 Bucket vaadittu" + }, + { + "id": "api.admin.test_email.subject", + "translation": "Mattermost - Sähköpostiasetusten testaaminen" + }, + { + "id": "api.admin.test_email.reenter_password", + "translation": "SMTP-palvelimen osoite, portti tai käyttäjätunnus on vaihtunut. Päivitä tiedot ja yritä uudelleen." + }, + { + "id": "api.admin.test_email.missing_server", + "translation": "SMTP-palvelin vaaditaan" + }, + { + "id": "api.admin.test_email.body", + "translation": "Näyttää siltä, että Mattermostin sähköpostiasetuksesi ovat oikein!" + }, + { + "id": "api.admin.saml.not_available.app_error", + "translation": "SAML 2.0 ei ole määritetty tai tuettu tällä palvelimella." + }, + { + "id": "api.admin.saml.metadata.app_error", + "translation": "Palveluntarjoajan metadataa rakennettaessa tapahtui virhe." + }, + { + "id": "api.admin.saml.failure_save_idp_certificate_file.app_error", + "translation": "Varmennetiedostoa ei voitu tallentaa." + }, + { + "id": "api.admin.remove_certificate.delete.app_error", + "translation": "Tapahtui virhe poistettaessa OAuth2 appia." + }, + { + "id": "api.admin.get_brand_image.storage.app_error", + "translation": "Kuvien tallennuksen asetuksia ei ole tehty." + }, + { + "id": "api.admin.file_read_error", + "translation": "Virhe luettaessa lokitiedostoa." + }, + { + "id": "api.admin.delete_brand_image.storage.not_found", + "translation": "Brandikuvaa ei pystytty poistamaan, ei löydy." + }, + { + "id": "api.admin.add_certificate.saving.app_error", + "translation": "Varmennetiedostoa ei voitu tallentaa." + }, + { + "id": "api.admin.add_certificate.open.app_error", + "translation": "Varmennetiedostoa ei voitu avata." + }, + { + "id": "api.admin.add_certificate.no_file.app_error", + "translation": "Ei tiedostoa pyynnön kohdassa 'certificate'." + }, + { + "id": "api.admin.add_certificate.array.app_error", + "translation": "Ei tiedostoa pyynnön kohdassa 'certificate'." + }, + { + "id": "September", + "translation": "Syyskuu" + }, + { + "id": "October", + "translation": "Lokakuu" + }, + { + "id": "November", + "translation": "Marraskuu" + }, + { + "id": "May", + "translation": "Toukokuu" + }, + { + "id": "March", + "translation": "Maaliskuu" + }, + { + "id": "June", + "translation": "Kesäkuu" + }, + { + "id": "July", + "translation": "Heinäkuu" + }, + { + "id": "January", + "translation": "Tammikuu" + }, + { + "id": "February", + "translation": "Helmikuu" + }, + { + "id": "December", + "translation": "Joulukuu" + }, + { + "id": "August", + "translation": "Elokuu" + }, + { + "id": "April", + "translation": "Huhtikuu" + }, + { + "id": "model.group.name.invalid_length.app_error", + "translation": "Nimen oltava 1-64 pientä kirjainta tai numeroa" + }, + { + "id": "web.error.unsupported_browser.system_browser_or", + "translation": "tai" + }, + { + "id": "api.cloud.request_error", + "translation": "Virhe CWS-pyynnön käsittelyssä." + }, + { + "id": "api.cloud.notify_admin_to_upgrade_error.already_notified", + "translation": "Ylläpidolle on jo ilmoitettu" + }, + { + "id": "api.cloud.cws_webhook_event_missing_error", + "translation": "Webhook tapahtumaa ei käsitelty. Se puuttuu tai on virheellinen." + }, + { + "id": "api.cloud.app_error", + "translation": "Sisäinen virhe pilvirajapintapyynnön aikana." + }, + { + "id": "api.channel.update_team_member_roles.changing_guest_role.app_error", + "translation": "Epäkelpo tiimijäsenyyden muuto: Vierasroolia ei voi lisätä tai poistaa manuaalisesti." + }, + { + "id": "api.channel.update_channel_privacy.default_channel_error", + "translation": "Oletuskanavaa ei voi asettaa yksityiseksi." + }, + { + "id": "api.channel.update_channel_member_roles.guest_and_user.app_error", + "translation": "Epäkelpo kanavajäsenyyden muutos: Käyttäjän rooli voi olla vieras tai käyttäjä, mutta ei molemmat." + }, + { + "id": "api.channel.update_channel_member_roles.changing_guest_role.app_error", + "translation": "Epäkelpo kanavajäsenyyden muutos: Vierasroolia ei voi lisätä tai poistaa manuaalisesti." + }, + { + "id": "api.channel.update_channel.typechange.app_error", + "translation": "Tätä kanavatyyppiä ei voida päivittää." + }, + { + "id": "api.channel.restore_channel.unarchived", + "translation": "{{.Username}} poisti kanavan arkistoinnin." + }, + { + "id": "api.channel.restore_channel.restored.app_error", + "translation": "Kanavaa ei voida arkistoida. Kanavaa ei ole arkistoitu." + }, + { + "id": "api.channel.remove_members.denied", + "translation": "Ryhmähallinta estää kanavajäsenyyden poistamisen käyttäjiltä: {{ .UserIDs }}" + }, + { + "id": "api.channel.remove_member.group_constrained.app_error", + "translation": "Käyttäjän poistaminen ryhmähallitusta kanavasta ei ole mahdollista." + }, + { + "id": "api.channel.patch_channel_moderations.cache_invalidation.error", + "translation": "Virhe välimuistin tyhjentämisessä" + }, + { + "id": "api.channel.move_channel.type.invalid", + "translation": "Suoria ja ryhmäviestikanavia ei voi siirtää" + }, + { + "id": "api.channel.guest_join_channel.post_and_forget", + "translation": "%v liittyi kanavalle vieraana." + }, + { + "id": "api.channel.create_channel.direct_channel.team_restricted_error", + "translation": "Suoraa kanavaa näiden käyttäjien välille ei voida luoda, koska heillä ei ole yhteistä tiimiä." + }, + { + "id": "api.channel.add_members.user_denied", + "translation": "Kanavalle liittyminen on ryhmän rajoitusten vuoksi estetty näiltä käyttäjiltä: {{ .UserIDs }}" + }, + { + "id": "api.bot.teams_channels.add_message_mobile", + "translation": "Lisää minut niihin tiimeihin ja kanaviin joissa haluat minun toimivan. Voit käyttää selainta tai Mattermostin työpöytäsovellusta." + }, + { + "id": "api.back_to_app", + "translation": "Takaisin sivustoon {{.SiteName}}" + }, + { + "id": "api.admin.saml.set_certificate_from_metadata.missing_content_type.app_error", + "translation": "Puuttuva sisältötyyppi." + }, + { + "id": "api.admin.saml.set_certificate_from_metadata.invalid_content_type.app_error", + "translation": "Epäkelpo sisältötyyppi." + }, + { + "id": "api.admin.saml.set_certificate_from_metadata.invalid_body.app_error", + "translation": "Epäkelpo sertifikaattiteksti." + }, + { + "id": "api.admin.saml.invalid_xml_missing_ssoservices.app_error", + "translation": "XML:stä puuttuu Identity Providerin SSO Services -osio." + }, + { + "id": "api.admin.saml.invalid_xml_missing_keydescriptor.app_error", + "translation": "XML:stä puuttuu Identity Providerin SSO Descriptor -osiot." + }, + { + "id": "api.admin.saml.invalid_xml_missing_idpssodescriptors.app_error", + "translation": "XML:stä puuttuu Identity Providerin SSO Descriptor -osio." + }, + { + "id": "api.admin.saml.failure_reset_authdata_to_email.app_error", + "translation": "AuthData kentän asettaminen Sähköpostiksi epäonnistui." + }, + { + "id": "api.admin.saml.failure_parse_idp_certificate.app_error", + "translation": "Identity Providerilta toimitettujen metatietojen tulkinnassa sertifikaatiksi ilmeni virhe." + }, + { + "id": "api.admin.saml.failure_get_metadata_from_idp.app_error", + "translation": "Metatietojen saanti Indentity Provider URL:stä epäonnistui." + }, + { + "id": "api.admin.ldap.not_available.app_error", + "translation": "LDAP ei ole käytettävissä." + }, + { + "id": "api.admin.add_certificate.parseform.app_error", + "translation": "Virhe multiform pyynnön tulkinnassa" + }, + { + "id": "Playbooks", + "translation": "Pelikirjat" + }, + { + "id": "Channels", + "translation": "Kanavat" + }, + { + "id": "Boards", + "translation": "Taulut" + } +] diff --git a/server/i18n/fil.json b/server/i18n/fil.json new file mode 100644 index 00000000000..0967ef424bc --- /dev/null +++ b/server/i18n/fil.json @@ -0,0 +1 @@ +{} diff --git a/server/i18n/fr.json b/server/i18n/fr.json index 04a155c82f0..09e782c9de7 100644 --- a/server/i18n/fr.json +++ b/server/i18n/fr.json @@ -4043,10 +4043,6 @@ "id": "model.emoji.user_id.app_error", "translation": "Id créateur invalide" }, - { - "id": "model.file_info.get.gif.app_error", - "translation": "Impossible de décoder le gif." - }, { "id": "model.file_info.is_valid.create_at.app_error", "translation": "Valeur invalide pour le paramètre « create_at »." @@ -8571,10 +8567,6 @@ "id": "app.prepackged-plugin.invalid_version.app_error", "translation": "La version du plugin pré-packagé n'a pas pu être traitée." }, - { - "id": "app.post.get_top_threads_for_user_since.app_error", - "translation": "Impossible d'obtenir le fils supérieurs pour l'utilisateur." - }, { "id": "api.license.request_trial_license.embargoed", "translation": "Nous n'avons pas pu traiter votre demande en raison de limitations pour les pays sous embargos. [Vous pouvez en savoir plus dans notre documentation](https://mattermost.com/pl/limitations-for-embargoed-countries), ou adressez-vous à legal@mattermost.com pour toute question relative aux limitations d'exportation." @@ -8679,14 +8671,6 @@ "id": "app.role.get_all.app_error", "translation": "Impossible d'obtenir tous les rôles." }, - { - "id": "app.recent_searches.app_error", - "translation": "Impossible de récupérer les recherches récentes" - }, - { - "id": "app.post.get_top_threads_for_team_since.app_error", - "translation": "Impossible d'obtenir le fils supérieurs pour l'équipe." - }, { "id": "app.post.analytics_teams_count.app_error", "translation": "Impossible d'obtenir l'utilisation des équipes" @@ -8699,10 +8683,6 @@ "id": "app.job.get_all_jobs_by_type_and_status.app_error", "translation": "Impossible d'obtenir tous les travaux par type et statuts." }, - { - "id": "app.insights.feature_disabled", - "translation": "La fonctionnalité des aperçus est désactivée." - }, { "id": "app.group.crud_permission", "translation": "Impossible de réaliser les opérations pour ce type de source." @@ -8767,10 +8747,6 @@ "id": "api.license_error", "translation": "l'API endpoint nécessite une licence" }, - { - "id": "api.insights.feature_disabled", - "translation": "Aperçus est derrière un drapeau de fonctionnalité qui n'est pas activé." - }, { "id": "app.user.update_update.app_error", "translation": " " diff --git a/server/i18n/fy.json b/server/i18n/fy.json new file mode 100644 index 00000000000..0967ef424bc --- /dev/null +++ b/server/i18n/fy.json @@ -0,0 +1 @@ +{} diff --git a/server/i18n/gl.json b/server/i18n/gl.json new file mode 100644 index 00000000000..12cad7fefca --- /dev/null +++ b/server/i18n/gl.json @@ -0,0 +1,274 @@ +[ + { + "id": "web.error.unsupported_browser.system_browser_or", + "translation": "ou" + }, + { + "id": "api.admin.add_certificate.open.app_error", + "translation": "Non foi posíbel abrir o ficheiro do certificado." + }, + { + "id": "api.admin.add_certificate.no_file.app_error", + "translation": "Non hai ningún ficheiro baixo «certificado» na solicitude." + }, + { + "id": "api.admin.add_certificate.array.app_error", + "translation": "Non hai ningún ficheiro baixo «certificado» na solicitude." + }, + { + "id": "September", + "translation": "Setembro" + }, + { + "id": "October", + "translation": "Outubro" + }, + { + "id": "November", + "translation": "Novembro" + }, + { + "id": "May", + "translation": "Maio" + }, + { + "id": "March", + "translation": "marzo" + }, + { + "id": "June", + "translation": "Xuño" + }, + { + "id": "July", + "translation": "Xullo" + }, + { + "id": "January", + "translation": "Xaneiro" + }, + { + "id": "February", + "translation": "Febreiro" + }, + { + "id": "December", + "translation": "Decembro" + }, + { + "id": "August", + "translation": "Agosto" + }, + { + "id": "April", + "translation": "Abril" + }, + { + "id": "api.emoji.create.too_large.app_error", + "translation": "Non foi posíbel crear o «emoji». A imaxe debe ter un tamaño inferior a 1 MB." + }, + { + "id": "api.oauth.get_access_token.bad_grant.app_error", + "translation": "invalid_request: «grant_type» incorrecto." + }, + { + "id": "api.oauth.get_access_token.bad_client_secret.app_error", + "translation": "invalid_request: Falta o «client_secret»." + }, + { + "id": "api.oauth.get_access_token.bad_client_id.app_error", + "translation": "invalid_request: «client_id» incorrecto." + }, + { + "id": "api.context.token_provided.app_error", + "translation": "A sesión non é OAuth, mais forneceuse un testemuño na cadea de consulta." + }, + { + "id": "api.context.invalid_token.error", + "translation": "O testemuño de sesión {{.Token}} non é válido, err={{.Error}}" + }, + { + "id": "api.team.leave.left", + "translation": "% v deixou o equipo." + }, + { + "id": "api.command_leave.name", + "translation": "abandonar" + }, + { + "id": "api.channel.leave.default.app_error", + "translation": "Non é posíbel abandonar a canle predeterminada {{.Channel}}." + }, + { + "id": "app.channel_member_history.log_leave_event.internal_error", + "translation": "Produciuse un fallo ao gravar o historial de membros da canle. Non foi posíbel actualizar o rexistro de entrada existente" + }, + { + "id": "api.command_leave.fail.app_error", + "translation": "Produciuse un erro ao abandonar a canle." + }, + { + "id": "api.command_leave.desc", + "translation": "Abandona a canle actual" + }, + { + "id": "api.channel.post_user_add_remove_message_and_forget.error", + "translation": "Non foi posíbel publicar a mensaxe de unirse/abandonar" + }, + { + "id": "api.channel.leave.direct.app_error", + "translation": "Non é posíbel abandonar unha canle de mensaxe directa." + }, + { + "id": "api.channel.leave.left", + "translation": "%v saíu da canle." + }, + { + "id": "api.channel.leave.last_member.app_error", + "translation": "Vostede é o único membro que queda, tente eliminar a canle privada no canto de abandonar." + }, + { + "id": "api.channel.create_default_channels.town_square", + "translation": "Praza pública" + }, + { + "id": "model.emoji.name.app_error", + "translation": "O nome debe ter de 1 a 64 caracteres alfanuméricos en minúscula." + }, + { + "id": "model.group.name.invalid_length.app_error", + "translation": "O nome debe ter de 1 a 64 caracteres alfanuméricos en minúscula." + }, + { + "id": "mfa.validate_token.authenticate.app_error", + "translation": "Testemuño AMF non válido." + }, + { + "id": "mfa.generate_qr_code.create_code.app_error", + "translation": "Produciuse un erro ao xerar o código QR." + }, + { + "id": "mfa.activate.bad_token.app_error", + "translation": "Testemuño AMF non válido." + }, + { + "id": "api.user.check_user_mfa.bad_code.app_error", + "translation": "Testemuño AMF non válido." + }, + { + "id": "api.user.activate_mfa.email_and_ldap_only.app_error", + "translation": "A AMF non está dispoñíbel para este tipo de conta." + }, + { + "id": "api.templates.mfa_change_subject", + "translation": "[{{.SiteName}}] Actualizouse a súa AMF" + }, + { + "id": "api.server.warn_metric.mfa.start_trial_notification_success.message", + "translation": "A súa proba de Enterprise xa está activa. Vaia a **Consola do sistema > Autenticación > AMF** para aplicar a autenticación de múltiples factores." + }, + { + "id": "mfa.mfa_disabled.app_error", + "translation": "Desactivouse a autenticación de múltiples factores neste servidor." + }, + { + "id": "api.templates.mfa_deactivated_body.title", + "translation": "Retirouse a autenticación de múltiples factores" + }, + { + "id": "api.templates.mfa_deactivated_body.info", + "translation": "Retirouse a autenticación de múltiples factores da súa conta en {{.SiteURL}}." + }, + { + "id": "api.templates.mfa_activated_body.title", + "translation": "Engadiuse a autenticación de múltiples factores" + }, + { + "id": "api.templates.mfa_activated_body.info", + "translation": "Engadiuse a autenticación de múltiples factores á súa conta en {{.SiteURL}}." + }, + { + "id": "api.server.warn_metric.mfa.notification_title", + "translation": "Aplicar a autenticación de múltiples factores" + }, + { + "id": "api.server.warn_metric.mfa.contact_us.email_body", + "translation": "Solicitude de contacto con Mattermost. Estou interesado en saber máis sobre como aplicar a autenticación de múltiples factores.\n" + }, + { + "id": "api.context.mfa_required.app_error", + "translation": "Neste servidor é necesaria a autenticación de múltiples factores." + }, + { + "id": "ent.cluster.save_config.error", + "translation": "A consola do sistema está configurada só para lectura cando a Alta dispoñibilidade está habilitada a menos que ReadOnlyConfig estea desactivado no ficheiro de configuración." + }, + { + "id": "api.server.warn_metric.number_of_active_users_500.start_trial.notification_success.message", + "translation": "A súa proba de Enterprise xa está activa. Vaia á consola do sistema para activar as funcións avanzadas." + }, + { + "id": "api.server.warn_metric.email_domain.start_trial_notification_success.message", + "translation": "A súa proba de Enterprise xa está activa. Vaia a **Consola do sistema > Autenticación > Acceso de convidado** para activar as contas de convidado." + }, + { + "id": "api.admin.saml.failure_save_idp_certificate_file.app_error", + "translation": "Non foi posíbel gardar o ficheiro do certificado." + }, + { + "id": "api.admin.saml.failure_parse_idp_certificate.app_error", + "translation": "Atopouse un falloao analizar a información de metadatos recibida do provedor de identidade nun certificado." + }, + { + "id": "api.admin.saml.failure_get_metadata_from_idp.app_error", + "translation": "Produciuse un fallo ao obter os metadatos do URL do provedor de identidade." + }, + { + "id": "api.admin.remove_certificate.delete.app_error", + "translation": "Produciuse un erro ao eliminar o certificado." + }, + { + "id": "api.admin.ldap.not_available.app_error", + "translation": "LDAP non está dispoñíbel." + }, + { + "id": "api.admin.get_brand_image.storage.app_error", + "translation": "O almacenamento de imaxes non está configurado." + }, + { + "id": "api.admin.file_read_error", + "translation": "Produciuse un erro ao ler o ficheiro de rexistro." + }, + { + "id": "api.admin.delete_brand_image.storage.not_found", + "translation": "Non é posíbel eliminar a imaxe de marca, non se atopou." + }, + { + "id": "api.admin.add_certificate.saving.app_error", + "translation": "Non foi posíbel gardar o ficheiro do certificado." + }, + { + "id": "api.admin.add_certificate.parseform.app_error", + "translation": "Produciuse un erro ao analizar a solicitude multiforme" + }, + { + "id": "api.templates.payment_failed_no_card.title", + "translation": "A súa factura de Mattermost Cloud está pendente de pago" + }, + { + "id": "api.templates.payment_failed_no_card.button", + "translation": "Pagar agora" + }, + { + "id": "api.templates.email_us_anytime_at", + "translation": "Envíenos un correo-e en calquera momento a " + }, + { + "id": "api.templates.copyright", + "translation": "© 2017 Mattermost, Inc. 855 El Camino Real, 13A-168, Palo Alto, CA, 94301" + }, + { + "id": "api.roles.patch_roles.not_allowed_permission.error", + "translation": "Un ou máis dos seguintes permisos que tenta engadir ou eliminar non está autorizado" + } +] diff --git a/server/i18n/gu.json b/server/i18n/gu.json new file mode 100644 index 00000000000..0967ef424bc --- /dev/null +++ b/server/i18n/gu.json @@ -0,0 +1 @@ +{} diff --git a/server/i18n/he.json b/server/i18n/he.json new file mode 100644 index 00000000000..72b2e80c624 --- /dev/null +++ b/server/i18n/he.json @@ -0,0 +1,74 @@ +[ + { + "id": "September", + "translation": "ספטמבר" + }, + { + "id": "October", + "translation": "אוקטובר" + }, + { + "id": "November", + "translation": "נובמבר" + }, + { + "id": "May", + "translation": "מאי" + }, + { + "id": "March", + "translation": "מרץ" + }, + { + "id": "June", + "translation": "יוני" + }, + { + "id": "July", + "translation": "יולי" + }, + { + "id": "January", + "translation": "ינואר" + }, + { + "id": "February", + "translation": "פברואר" + }, + { + "id": "December", + "translation": "דצמבר" + }, + { + "id": "August", + "translation": "אוגוסט" + }, + { + "id": "April", + "translation": "אפריל" + }, + { + "id": "api.admin.saml.set_certificate_from_metadata.invalid_body.app_error", + "translation": "טקסט אישור לא חוקי." + }, + { + "id": "api.admin.saml.not_available.app_error", + "translation": "SAML 2.0 אינו מוגדר או נתמך בשרת זה." + }, + { + "id": "api.admin.saml.metadata.app_error", + "translation": "אירעה שגיאה בעת בניית הנתונים של ספק השירות." + }, + { + "id": "api.admin.add_certificate.open.app_error", + "translation": "לא ניתן היה לפתוח את קובץ התעודה." + }, + { + "id": "api.admin.add_certificate.no_file.app_error", + "translation": "אין קובץ תחת 'תעודה' בבקשה." + }, + { + "id": "api.admin.add_certificate.array.app_error", + "translation": "אין קובץ תחת 'תעודה' בבקשה." + } +] diff --git a/server/i18n/hi.json b/server/i18n/hi.json new file mode 100644 index 00000000000..d24b55fb825 --- /dev/null +++ b/server/i18n/hi.json @@ -0,0 +1,8690 @@ +[ + { + "id": "web.error.unsupported_browser.system_browser_or", + "translation": "या" + }, + { + "id": "August", + "translation": "अगस्त" + }, + { + "id": "April", + "translation": "अप्रैल" + }, + { + "id": "api.admin.saml.invalid_xml_missing_ssoservices.app_error", + "translation": "एक्सएमएल में गुम पहचान प्रदाता एसएसओ सेवा नोड।" + }, + { + "id": "api.admin.saml.invalid_xml_missing_keydescriptor.app_error", + "translation": "एक्सएमएल में गुम पहचान प्रदाता कुंजी डिस्क्रिप्टर नोड।" + }, + { + "id": "api.admin.saml.invalid_xml_missing_idpssodescriptors.app_error", + "translation": "एक्सएमएल में गुम पहचान प्रदाता एसएसओ डिस्क्रिप्टर नोड।" + }, + { + "id": "api.admin.saml.failure_save_idp_certificate_file.app_error", + "translation": "प्रमाणपत्र फ़ाइल सहेजी नहीं जा सकी." + }, + { + "id": "api.admin.saml.failure_reset_authdata_to_email.app_error", + "translation": "ऑथडाटा फ़ील्ड को ईमेल पर रीसेट करने में विफल।" + }, + { + "id": "api.admin.saml.failure_parse_idp_certificate.app_error", + "translation": "पहचान प्रदाता से प्राप्त मेटाडेटा जानकारी को प्रमाणपत्र में पार्स करते समय विफलता का सामना करना पड़ा।" + }, + { + "id": "api.admin.saml.failure_get_metadata_from_idp.app_error", + "translation": "पहचान प्रदाता यूआरएल से मेटाडेटा प्राप्त करने में विफल।" + }, + { + "id": "api.admin.remove_certificate.delete.app_error", + "translation": "प्रमाणपत्र हटाते समय एक त्रुटि हुई।" + }, + { + "id": "api.admin.ldap.not_available.app_error", + "translation": "एलडीएपी उपलब्ध नहीं है।" + }, + { + "id": "api.admin.get_brand_image.storage.app_error", + "translation": "छवि संग्रहण कॉन्फ़िगर नहीं किया गया है।" + }, + { + "id": "api.admin.file_read_error", + "translation": "लॉग फ़ाइल पढ़ने में त्रुटि।" + }, + { + "id": "api.admin.delete_brand_image.storage.not_found", + "translation": "ब्रांड छवि को हटाने में असमर्थ, नहीं मिला।" + }, + { + "id": "api.admin.add_certificate.saving.app_error", + "translation": "प्रमाणपत्र फ़ाइल सहेजी नहीं जा सकी." + }, + { + "id": "api.admin.add_certificate.parseform.app_error", + "translation": "बहुरूप अनुरोध को पार्स करने में त्रुटि" + }, + { + "id": "api.admin.add_certificate.open.app_error", + "translation": "प्रमाणपत्र फ़ाइल नहीं खोल सका." + }, + { + "id": "api.admin.add_certificate.no_file.app_error", + "translation": "अनुरोध में 'प्रमाणपत्र' के तहत कोई फ़ाइल नहीं।" + }, + { + "id": "api.admin.add_certificate.array.app_error", + "translation": "अनुरोध में 'प्रमाणपत्र' के तहत कोई फ़ाइल नहीं।" + }, + { + "id": "September", + "translation": "सितंबर" + }, + { + "id": "October", + "translation": "अक्टूबर" + }, + { + "id": "November", + "translation": "नवंबर" + }, + { + "id": "May", + "translation": "मई" + }, + { + "id": "March", + "translation": "मार्च" + }, + { + "id": "June", + "translation": "जून" + }, + { + "id": "July", + "translation": "जुलाई" + }, + { + "id": "January", + "translation": "जनवरी" + }, + { + "id": "February", + "translation": "फ़रवरी" + }, + { + "id": "December", + "translation": "दिसंबर" + }, + { + "id": "api.admin.saml.metadata.app_error", + "translation": "सेवा प्रदाता मेटाडेटा बनाते समय कोई त्रुटि उत्पन्न हुई।" + }, + { + "id": "web.incoming_webhook.user.app_error", + "translation": "उपयोगकर्ता नहीं मिल सका." + }, + { + "id": "api.command_invite.desc", + "translation": "किसी उपयोगकर्ता को चैनल में आमंत्रित करें" + }, + { + "id": "api.emoji.create.duplicate.app_error", + "translation": "इमोजी बनाने में असमर्थ। इसी नाम का एक और इमोजी पहले से मौजूद है।" + }, + { + "id": "app.import.validate_channel_import_data.scheme_invalid.error", + "translation": "चैनल के लिए अमान्य योजना का नाम।" + }, + { + "id": "app.import.validate_channel_import_data.purpose_length.error", + "translation": "चैनल का उद्देश्य बहुत लंबा है." + }, + { + "id": "app.import.validate_channel_import_data.name_missing.error", + "translation": "आवश्यक चैनल गुण गुम है: name" + }, + { + "id": "app.import.validate_channel_import_data.name_length.error", + "translation": "चैनल का नाम बहुत लंबा है." + }, + { + "id": "app.import.validate_channel_import_data.name_characters.error", + "translation": "चैनल के नाम में अमान्य वर्ण हैं." + }, + { + "id": "app.import.validate_channel_import_data.header_length.error", + "translation": "चैनल हेडर बहुत लंबा है." + }, + { + "id": "app.import.validate_channel_import_data.display_name_length.error", + "translation": "चैनल display_name अनुमत लंबाई सीमा के भीतर नहीं है।" + }, + { + "id": "ent.cluster.json_encode.error", + "translation": "JSON अनुरोध को मार्शल करते समय त्रुटि हुई" + }, + { + "id": "ent.api.post.send_notifications_and_forget.push_image_only", + "translation": " एक फाइल संलग्न की।" + }, + { + "id": "ent.actiance.export.marshalToXml.appError", + "translation": "Export को XML में बदलने में असमर्थ।" + }, + { + "id": "ent.account_migration.get_saml_users_failed", + "translation": "SAML उपयोगकर्ता प्राप्त करने में असमर्थ।" + }, + { + "id": "ent.account_migration.get_all_failed", + "translation": "उपयोगकर्ता प्राप्त करने में असमर्थ।" + }, + { + "id": "brand.save_brand_image.save_image.app_error", + "translation": "आपके फ़ाइल संग्रहण में छवि फ़ाइल लिखने में असमर्थ। कृपया अपने संपर्क की जांच करे और फिर से प्रयास करें।" + }, + { + "id": "brand.save_brand_image.open.app_error", + "translation": "कस्टम ब्रांड छवि अपलोड करने में असमर्थ. सुनिश्चित करें कि छवि का आकार 2 एमबी से कम है और पुनः प्रयास करें।" + }, + { + "id": "brand.save_brand_image.encode.app_error", + "translation": "छवि डेटा को पीएनजी प्रारूप में बदलने में असमर्थ। कृपया पुन: प्रयास करें।" + }, + { + "id": "brand.save_brand_image.decode.app_error", + "translation": "छवि डेटा को डीकोड करने में असमर्थ।" + }, + { + "id": "bleveengine.stop_user_index.error", + "translation": "उपयोगकर्ता इंडेक्स को बंद करने में विफल।" + }, + { + "id": "bleveengine.stop_post_index.error", + "translation": "पोस्ट इंडेक्स को बंद करने में विफल।" + }, + { + "id": "bleveengine.stop_channel_index.error", + "translation": "चैनल अनुक्रमणिका को बंद करने में विफल।" + }, + { + "id": "bleveengine.search_users_in_team.error", + "translation": "उपयोगकर्ता खोज पूर्ण करने में विफल." + }, + { + "id": "bleveengine.search_users_in_channel.uchan.error", + "translation": "उपयोगकर्ता खोज पूर्ण करने में विफल." + }, + { + "id": "bleveengine.search_users_in_channel.nuchan.error", + "translation": "उपयोगकर्ता खोज पूर्ण करने में विफल." + }, + { + "id": "bleveengine.search_posts.error", + "translation": "पोस्ट खोज पूर्ण करने में विफल." + }, + { + "id": "bleveengine.search_channels.error", + "translation": "चैनल खोज पूर्ण करने में विफल." + }, + { + "id": "web.incoming_webhook.text.app_error", + "translation": "कोई टेक्स्ट निर्दिष्ट नहीं है।" + }, + { + "id": "web.incoming_webhook.split_props_length.app_error", + "translation": "वेबहुक प्रॉप्स को {{.Max}} कैरेक्टर भागों में विभाजित करने में असमर्थ।" + }, + { + "id": "web.incoming_webhook.permissions.app_error", + "translation": "अनुपयुक्त चैनल अनुमतियां।" + }, + { + "id": "web.incoming_webhook.parse.app_error", + "translation": "आने वाले डेटा को पार्स करने में असमर्थ।" + }, + { + "id": "web.incoming_webhook.invalid.app_error", + "translation": "अमान्य वेबहुक." + }, + { + "id": "web.incoming_webhook.disabled.app_error", + "translation": "आने वाले वेबहुक को सिस्टम व्यवस्थापक द्वारा अक्षम कर दिया गया है।" + }, + { + "id": "web.incoming_webhook.channel_locked.app_error", + "translation": "इस वेबहुक को अनुरोधित चैनल पर पोस्ट करने की अनुमति नहीं है।" + }, + { + "id": "web.incoming_webhook.channel.app_error", + "translation": "चैनल नहीं मिला." + }, + { + "id": "web.get_access_token.internal_saving.app_error", + "translation": "उपयोगकर्ता पहुंच डेटा अपडेट करने में असमर्थ।" + }, + { + "id": "web.error.unsupported_browser.system_browser_make_default", + "translation": "डिफ़ॉल्ट बनाना" + }, + { + "id": "web.error.unsupported_browser.open_system_browser.edge", + "translation": "ओपन एज" + }, + { + "id": "web.error.unsupported_browser.no_longer_support_version", + "translation": "आपके ब्राउज़र का यह संस्करण अब मैटरमोस्ट द्वारा समर्थित नहीं है" + }, + { + "id": "web.error.unsupported_browser.no_longer_support", + "translation": "यह ब्राउज़र अब मैटरमोस्ट द्वारा समर्थित नहीं है" + }, + { + "id": "web.error.unsupported_browser.min_os_version.windows", + "translation": "विंडोज़ 7+" + }, + { + "id": "web.error.unsupported_browser.min_os_version.mac", + "translation": "मैक ओएस 10.14+" + }, + { + "id": "web.error.unsupported_browser.min_browser_version.safari", + "translation": "विवरण 12+" + }, + { + "id": "web.error.unsupported_browser.min_browser_version.firefox", + "translation": "विवरण 78+" + }, + { + "id": "web.error.unsupported_browser.min_browser_version.edge", + "translation": "विवरण 44+" + }, + { + "id": "web.error.unsupported_browser.min_browser_version.chrome", + "translation": "विवरण 89+" + }, + { + "id": "web.error.unsupported_browser.learn_more", + "translation": "समर्थित ब्राउज़रों के बारे में अधिक जानें।" + }, + { + "id": "web.error.unsupported_browser.install_guide.windows", + "translation": "गाइड इंस्टॉल करें" + }, + { + "id": "web.error.unsupported_browser.install_guide.mac", + "translation": "गाइड इंस्टॉल करें" + }, + { + "id": "web.error.unsupported_browser.download_the_app", + "translation": "ऐप डाउनलोड करें" + }, + { + "id": "web.error.unsupported_browser.download_app_or_upgrade_browser", + "translation": "बेहतर अनुभव के लिए मैटरमॉस्ट ऐप डाउनलोड करें या समर्थित ब्राउज़र का उपयोग करें।" + }, + { + "id": "web.error.unsupported_browser.download", + "translation": "ऐप डाउनलोड करें" + }, + { + "id": "web.error.unsupported_browser.browser_title.safari", + "translation": "सफारी" + }, + { + "id": "web.error.unsupported_browser.browser_title.firefox", + "translation": "फ़ायर्फ़ॉक्स" + }, + { + "id": "web.error.unsupported_browser.browser_title.chrome", + "translation": "गूगल क्रोम" + }, + { + "id": "web.error.unsupported_browser.browser_get_latest.safari", + "translation": "सब से विलंबित सफारी ब्राउज़र प्राप्त करें" + }, + { + "id": "web.error.unsupported_browser.browser_get_latest.firefox", + "translation": "सब से विलंबित फ़ायरफ़ॉक्स ब्राउज़र प्राप्त करें" + }, + { + "id": "web.error.unsupported_browser.browser_get_latest.chrome", + "translation": "सब से विलंबित क्रोम ब्राउज़र प्राप्त करें" + }, + { + "id": "web.command_webhook.parse.app_error", + "translation": "आने वाले डेटा को पार्स करने में असमर्थ।" + }, + { + "id": "web.command_webhook.command.app_error", + "translation": "कमांड नहीं मिल सका।" + }, + { + "id": "store.sql_user.get_for_login.app_error", + "translation": "आपकी साख से मेल खाता कोई मौजूदा खाता खोजने में असमर्थ। इस टीम को शामिल होने के लिए टीम के मालिक से आमंत्रण की आवश्यकता हो सकती है।" + }, + { + "id": "store.sql_team.save_member.exists.app_error", + "translation": "उस आईडी वाला एक टीम सदस्य पहले से मौजूद है।" + }, + { + "id": "store.sql_post.search.disabled", + "translation": "इस सर्वर पर खोजना अक्षम कर दिया गया है। कृपया अपने सिस्टम व्यवस्थापक से संपर्क करें।" + }, + { + "id": "store.sql_command.update.missing.app_error", + "translation": "कमांड मौजूद नहीं है।" + }, + { + "id": "store.sql_command.save.get.app_error", + "translation": "कमांड प्राप्त करने में असमर्थ।" + }, + { + "id": "store.sql_command.get.missing.app_error", + "translation": "कमांड मौजूद नहीं है।" + }, + { + "id": "store.sql_channel.save_direct_channel.not_direct.app_error", + "translation": "SaveDirectChannel के साथ प्रत्यक्ष चैनल बनाने का प्रयास नहीं किया गया।" + }, + { + "id": "store.sql_channel.save_channel.limit.app_error", + "translation": "आप अनुमत चैनलों की संख्या की सीमा तक पहुंच गए हैं।" + }, + { + "id": "store.sql_channel.save_channel.exists.app_error", + "translation": "उसी टीम में उस नाम का एक चैनल पहले से मौजूद है।" + }, + { + "id": "store.sql_channel.save.direct_channel.app_error", + "translation": "डायरेक्ट चैनल बनाने के लिए SaveDirectChannel का उपयोग करें।" + }, + { + "id": "store.sql_channel.get.existing.app_error", + "translation": "मौजूदा चैनल खोजने में असमर्थ।" + }, + { + "id": "store.sql_bot.get.missing.app_error", + "translation": "बॉट मौजूद नहीं है।" + }, + { + "id": "sharedchannel.permalink.not_found", + "translation": "इस पोस्ट में अन्य चैनलों के लिए परमालिंक हैं जो अन्य साइटों के उपयोगकर्ताओं के लिए दृश्यमान नहीं हो सकते हैं।" + }, + { + "id": "plugin_api.send_mail.missing_subject", + "translation": "ईमेल विषय गुम." + }, + { + "id": "plugin_api.send_mail.missing_htmlbody", + "translation": "HTML बॉडी गुम है।" + }, + { + "id": "plugin_api.get_file_link.no_post.app_error", + "translation": "फ़ाइल के लिए सार्वजनिक लिंक प्राप्त करने में असमर्थ। फ़ाइल को उस पोस्ट के साथ संलग्न किया जाना चाहिए जिसे पढ़ा जा सके।" + }, + { + "id": "plugin_api.get_file_link.disabled.app_error", + "translation": "सार्वजनिक लिंक अक्षम कर दिए गए हैं।" + }, + { + "id": "plugin_api.bot_cant_create_bot", + "translation": "बॉट उपयोगकर्ता बॉट उपयोगकर्ता नहीं बना सकता।" + }, + { + "id": "plugin.api.update_user_status.bad_status", + "translation": "उपयोगकर्ता स्थिति सेट करने में असमर्थ। अज्ञात उपयोगकर्ता स्थिति।" + }, + { + "id": "oauth.gitlab.tos.error", + "translation": "GitLab की सेवा की शर्तें अपडेट हो गई हैं। कृपया उन्हें स्वीकार करने के लिए gitlab.com पर जाएं और फिर Mattermost में फिर से लॉग इन करने का प्रयास करें।" + }, + { + "id": "model.websocket_client.connect_fail.app_error", + "translation": "WebSocket सर्वर से कनेक्ट करने में असमर्थ।" + }, + { + "id": "model.utils.decode_json.app_error", + "translation": "डिकोड नहीं कर सका।" + }, + { + "id": "model.user_access_token.is_valid.id.app_error", + "translation": "आईडी के लिए अमान्य मान।" + }, + { + "id": "model.user_access_token.is_valid.description.app_error", + "translation": "अमान्य विवरण, 255 या उससे कम वर्णों का होना चाहिए." + }, + { + "id": "model.user.is_valid.username.app_error", + "translation": "उपयोगकर्ता नाम एक अक्षर से शुरू होना चाहिए, और इसमें 3 से 22 लोअरकेस वर्ण होने चाहिए जो अंक, अक्षरों और प्रतीकों \"।\", \"-\", और \"_\" से बने हों।" + }, + { + "id": "model.user.is_valid.roles_limit.app_error", + "translation": "{{.Limit}} वर्णों से अधिक लंबी अमान्य उपयोगकर्ता भूमिकाएँ।" + }, + { + "id": "model.user.is_valid.pwd_uppercase_symbol.app_error", + "translation": "आपके पासवर्ड में कम से कम {{.Min}} वर्ण होने चाहिए जो कम से कम एक बड़े अक्षर और कम से कम एक प्रतीक (जैसे \"~!@#$%^&*()\") से बने हों।" + }, + { + "id": "model.user.is_valid.pwd_uppercase_number_symbol.app_error", + "translation": "आपके पासवर्ड में कम से कम एक बड़े अक्षर, कम से कम एक अंक और कम से कम एक प्रतीक (जैसे \"~!@#$%^&*()\") से बने कम से कम {{.Min}} वर्ण होने चाहिए।" + }, + { + "id": "model.user.is_valid.pwd_uppercase_number.app_error", + "translation": "आपके पासवर्ड में कम से कम एक बड़े अक्षर और कम से कम एक अंक से बने कम से कम {{.Min}} वर्ण होने चाहिए।" + }, + { + "id": "model.user.is_valid.pwd_uppercase.app_error", + "translation": "आपके पासवर्ड में कम से कम एक बड़े अक्षर से बने कम से कम {{.Min}} वर्ण होने चाहिए।" + }, + { + "id": "model.user.is_valid.pwd_symbol.app_error", + "translation": "आपके पासवर्ड में कम से कम एक प्रतीक (जैसे \"~!@#$%^&*()\") से बने कम से कम {{.Min}} वर्ण होने चाहिए।" + }, + { + "id": "model.user.is_valid.pwd_number_symbol.app_error", + "translation": "आपके पासवर्ड में कम से कम एक अंक और कम से कम एक प्रतीक (जैसे \"~!@#$%^&*()\") से बने कम से कम {{.Min}} वर्ण होने चाहिए।" + }, + { + "id": "model.user.is_valid.pwd_number.app_error", + "translation": "आपके पासवर्ड में कम से कम एक अंक से बने कम से कम {{.Min}} वर्ण होने चाहिए।" + }, + { + "id": "model.user.is_valid.pwd_lowercase_uppercase_symbol.app_error", + "translation": "आपके पासवर्ड में कम से कम {{.Min}} वर्ण होने चाहिए जो कम से कम एक छोटे अक्षर, कम से कम एक बड़े अक्षर और कम से कम एक प्रतीक (जैसे \"~!@#$%^&*()\") से बने हों।" + }, + { + "id": "model.user.is_valid.pwd_lowercase_uppercase_number_symbol.app_error", + "translation": "आपके पासवर्ड में कम से कम एक लोअरकेस अक्षर, कम से कम एक अपरकेस अक्षर, कम से कम एक अंक और कम से कम एक प्रतीक (जैसे \"~!@#$%^&*) से बना कम से कम {{.Min}} वर्ण होना चाहिए। ()\")।" + }, + { + "id": "model.user.is_valid.pwd_lowercase_uppercase_number.app_error", + "translation": "आपके पासवर्ड में कम से कम एक छोटे अक्षर, कम से कम एक बड़े अक्षर और कम से कम एक अंक से बने कम से कम {{.Min}} वर्ण होने चाहिए।" + }, + { + "id": "model.user.is_valid.pwd_lowercase_uppercase.app_error", + "translation": "आपके पासवर्ड में कम से कम एक लोअरकेस अक्षर और कम से कम एक अपरकेस अक्षर से बना कम से कम {{.Min}} वर्ण होना चाहिए।" + }, + { + "id": "model.user.is_valid.pwd_lowercase_symbol.app_error", + "translation": "आपके पासवर्ड में कम से कम {{.Min}} वर्ण होने चाहिए जो कम से कम एक छोटे अक्षर और कम से कम एक प्रतीक (जैसे \"~!@#$%^&*()\") से बने हों।" + }, + { + "id": "model.user.is_valid.pwd_lowercase_number_symbol.app_error", + "translation": "आपके पासवर्ड में कम से कम {{.Min}} वर्ण होने चाहिए जो कम से कम एक छोटे अक्षर, कम से कम एक अंक और कम से कम एक प्रतीक (जैसे \"~!@#$%^&*()\") से बने हों।" + }, + { + "id": "model.user.is_valid.pwd_lowercase_number.app_error", + "translation": "आपके पासवर्ड में कम से कम {{.Min}} वर्ण होने चाहिए जो कम से कम एक छोटे अक्षर और कम से कम एक अंक से बने हों।" + }, + { + "id": "model.user.is_valid.pwd_lowercase.app_error", + "translation": "आपके पासवर्ड में कम से कम एक लोअरकेस अक्षर से बने कम से कम {{.Min}} वर्ण होने चाहिए।" + }, + { + "id": "model.user.is_valid.pwd.app_error", + "translation": "आपके पासवर्ड में कम से कम {{.Min}} वर्ण होने चाहिए।" + }, + { + "id": "model.user.is_valid.position.app_error", + "translation": "अमान्य स्थिति: 128 वर्णों से अधिक नहीं होनी चाहिए।" + }, + { + "id": "model.user.is_valid.nickname.app_error", + "translation": "अमान्य उपनाम।" + }, + { + "id": "model.user.is_valid.marshal.app_error", + "translation": "फ़ील्ड को JSON में एन्कोड करने में असफलता" + }, + { + "id": "api.file.no_driver.app_error", + "translation": "कोई फ़ाइल ड्राइवर चुना नहीं गया है।" + }, + { + "id": "api.file.get_public_link.no_post.app_error", + "translation": "फ़ाइल के लिए सार्वजनिक लिंक प्राप्त करने में असमर्थ। फ़ाइल को उस पोस्ट के साथ संलग्न किया जाना चाहिए जिसे वर्तमान उपयोगकर्ता द्वारा पढ़ा जा सके।" + }, + { + "id": "api.file.get_public_link.disabled.app_error", + "translation": "सार्वजनिक लिंक अक्षम कर दिए गए हैं।" + }, + { + "id": "api.file.get_file_thumbnail.no_thumbnail.app_error", + "translation": "फ़ाइल में थंबनेल इमेज नहीं है।" + }, + { + "id": "api.file.get_file.public_invalid.app_error", + "translation": "सार्वजनिक लिंक मान्य प्रतीत नहीं होता है।" + }, + { + "id": "api.file.attachments.disabled.app_error", + "translation": "इस सर्वर पर फ़ाइल अटैचमेंट अक्षम कर दिए गए हैं।" + }, + { + "id": "api.error_get_first_admin_visit_marketplace_status", + "translation": "स्टोर से पहले व्यवस्थापक विज़िट मार्केटप्लेस स्थिति को पुनर्प्राप्त करने का प्रयास करने में त्रुटि।" + }, + { + "id": "api.emoji.upload.open.app_error", + "translation": "इमोजी बनाने में असमर्थ. संलग्न इमेज को खोलने का प्रयास करते समय कोई त्रुटि उत्पन्न हुई।" + }, + { + "id": "api.emoji.upload.large_image.too_large.app_error", + "translation": "इमोजी बनाने में असमर्थ। इमेज {{.MaxWidth}} से {{.MaxHeight}} से छोटी होनी चाहिए।" + }, + { + "id": "api.emoji.upload.large_image.gif_encode_error", + "translation": "इमोजी बनाने में असमर्थ। GIF इमेज को एन्कोड करने का प्रयास करते समय कोई त्रुटि उत्पन्न हुई।" + }, + { + "id": "api.emoji.upload.large_image.gif_decode_error", + "translation": "इमोजी बनाने में असमर्थ। GIF इमेज को डीकोड करने का प्रयास करते समय कोई त्रुटि उत्पन्न हुई|" + }, + { + "id": "api.emoji.upload.large_image.encode_error", + "translation": "इमोजी बनाने में असमर्थ।इमेज को एन्कोड करने का प्रयास करते समय एक त्रुटि उत्पन्न हुई।" + }, + { + "id": "api.emoji.upload.large_image.decode_error", + "translation": "इमोजी बनाने में असमर्थ।इमेज को डीकोड करने का प्रयास करते समय एक त्रुटि उत्पन्न हुई।" + }, + { + "id": "api.emoji.upload.image.app_error", + "translation": "इमोजी बनाने में असमर्थ। फ़ाइल PNG, JPEG या GIF होनी चाहिए." + }, + { + "id": "api.emoji.storage.app_error", + "translation": "फ़ाइल संग्रहण ठीक से कॉन्फ़िगर नहीं किया गया है। कृपया S3 या स्थानीय सर्वर फ़ाइल संग्रहण के लिए कॉन्फ़िगर करें।" + }, + { + "id": "api.emoji.get_image.read.app_error", + "translation": "इमोजी के लिए इमेज फ़ाइल पढ़ने में असमर्थ।" + }, + { + "id": "api.emoji.get_image.decode.app_error", + "translation": "इमोजी के लिए छवि फ़ाइल को डीकोड करने में असमर्थ।" + }, + { + "id": "api.emoji.disabled.app_error", + "translation": "कस्टम इमोजी को सिस्टम एडमिन ने बंद कर दिया है." + }, + { + "id": "api.emoji.create.too_large.app_error", + "translation": "इमोजी बनाने में असमर्थ। छवि का आकार 1 एमबी से कम होना चाहिए।" + }, + { + "id": "api.emoji.create.parse.app_error", + "translation": "इमोजी बनाने में असमर्थ। अनुरोध समझ में नहीं आया।" + }, + { + "id": "api.emoji.create.other_user.app_error", + "translation": "अमान्य उपयोगकर्ता आइ डी।" + }, + { + "id": "api.emoji.create.internal_error", + "translation": "server_error: इमोजी बनाने में आंतरिक सर्वर त्रुटि का सामना करना पड़ा।" + }, + { + "id": "api.email_batching.send_batched_email_notification.title", + "translation": "आपके पास नए मैसेजिस हैं" + }, + { + "id": "api.email_batching.send_batched_email_notification.time", + "translation": "{{.Hour}}:{{.Minute}} {{.TimeZone}}" + }, + { + "id": "api.email_batching.send_batched_email_notification.subTitle", + "translation": "अपने नए मैसेजो के सारांश के लिए नीचे देखें।" + }, + { + "id": "api.email_batching.send_batched_email_notification.messageButton", + "translation": "यह मैसेज देखें" + }, + { + "id": "api.email_batching.send_batched_email_notification.button", + "translation": "ओपन मैटरमोस्ट" + }, + { + "id": "api.email_batching.add_notification_email_to_batch.disabled.app_error", + "translation": "ईमेल बैचिंग को सिस्टम व्यवस्थापक द्वारा अक्षम कर दिया गया है।" + }, + { + "id": "api.email.send_warn_metric_ack.missing_server.app_error", + "translation": "SMTP Server की आवश्यकता है" + }, + { + "id": "api.email.send_warn_metric_ack.invalid_warn_metric.app_error", + "translation": "वाॅन् मीट्रिक नहीं मिल सका।" + }, + { + "id": "api.email.send_warn_metric_ack.failure.app_error", + "translation": "व्यवस्थापक पावती ईमेल भेजने में विफलता" + }, + { + "id": "api.create_terms_of_service.empty_text.app_error", + "translation": "कृपया अपनी कस्टम सेवा की शर्तों के लिए टेक्स्ट दर्ज करें।" + }, + { + "id": "api.create_terms_of_service.custom_terms_of_service_disabled.app_error", + "translation": "कस्टम सेवा की शर्तें सुविधा अक्षम है।" + }, + { + "id": "api.context.session_expired.app_error", + "translation": "अमान्य या समाप्त सत्र, कृपया फिर से लॉगिन करें।" + }, + { + "id": "api.context.server_busy.app_error", + "translation": "सर्वर व्यस्त है, गैर-महत्वपूर्ण सेवाएं अस्थायी रूप से अनुपलब्ध हैं।" + }, + { + "id": "api.context.remote_id_missing.app_error", + "translation": "सुरक्षित कनेक्शन आईडी गायब है।" + }, + { + "id": "api.context.remote_id_mismatch.app_error", + "translation": "सुरक्षित कनेक्शन आईडी बेमेल।" + }, + { + "id": "api.context.remote_id_invalid.app_error", + "translation": "सुरक्षित कनेक्शन आईडी खोजने में असमर्थ {{.RemoteId}}।" + }, + { + "id": "api.context.permissions.app_error", + "translation": "आपके पास उचित अनुमतियां नहीं हैं।" + }, + { + "id": "app.import.validate_post_import_data.message_missing.error", + "translation": "आवश्यक पोस्ट संपत्ति गुम है: Message." + }, + { + "id": "app.import.validate_post_import_data.message_length.error", + "translation": "Post Message संपत्ति अधिकतम अनुमत लंबाई से अधिक लंबी है।" + }, + { + "id": "app.import.validate_post_import_data.create_at_zero.error", + "translation": "पोस्ट CreateAt संपत्ति शून्य नहीं होनी चाहिए।" + }, + { + "id": "app.import.validate_post_import_data.create_at_missing.error", + "translation": "आवश्यक पोस्ट प्रॉपर्टी गुम है: create_at." + }, + { + "id": "app.import.validate_post_import_data.channel_missing.error", + "translation": "आवश्यक पोस्ट संपत्ति गुम है: Channel." + }, + { + "id": "app.import.validate_emoji_import_data.empty.error", + "translation": "इमोजी डेटा खाली आयात करें।" + }, + { + "id": "app.import.validate_direct_post_import_data.user_missing.error", + "translation": "आवश्यक प्रत्यक्ष पोस्ट संपत्ति गुम है: user" + }, + { + "id": "app.import.validate_direct_post_import_data.unknown_flagger.error", + "translation": "डायरेक्ट पोस्ट को केवल उस चैनल के सदस्य ही फ़्लैग कर सकते हैं जिसमें वह है। \"{{.Username}}\" सदस्य नहीं है।" + }, + { + "id": "app.import.validate_direct_post_import_data.message_missing.error", + "translation": "आवश्यक प्रत्यक्ष पोस्ट संपत्ति गुम है: message" + }, + { + "id": "app.import.validate_direct_post_import_data.message_length.error", + "translation": "मैसेज बहुत लंबा है" + }, + { + "id": "app.import.validate_direct_post_import_data.create_at_zero.error", + "translation": "CreateAt 0 से बड़ा होना चाहिए" + }, + { + "id": "app.import.validate_direct_post_import_data.create_at_missing.error", + "translation": "आवश्यक प्रत्यक्ष पोस्ट संपत्ति गुम है: create_at" + }, + { + "id": "app.import.validate_direct_post_import_data.channel_members_too_many.error", + "translation": "डायरेक्ट पोस्ट चैनल सदस्यों की सूची में बहुत अधिक आइटम हैं" + }, + { + "id": "app.import.validate_direct_post_import_data.channel_members_too_few.error", + "translation": "डायरेक्ट पोस्ट चैनल सदस्यों की सूची में बहुत कम आइटम हैं" + }, + { + "id": "app.import.validate_direct_post_import_data.channel_members_required.error", + "translation": "आवश्यक प्रत्यक्ष पोस्ट संपत्ति गुम है: channel_members" + }, + { + "id": "app.import.validate_direct_channel_import_data.unknown_favoriter.error", + "translation": "डायरेक्ट चैनल को केवल सदस्य ही पसंद कर सकते हैं। \"{{.Username}}\" सदस्य नहीं है।" + }, + { + "id": "app.import.validate_direct_channel_import_data.members_too_many.error", + "translation": "प्रत्यक्ष चैनल सदस्यों की सूची में बहुत अधिक आइटम हैं" + }, + { + "id": "app.import.validate_direct_channel_import_data.members_too_few.error", + "translation": "प्रत्यक्ष चैनल सदस्यों की सूची में बहुत कम आइटम हैं" + }, + { + "id": "app.import.validate_direct_channel_import_data.members_required.error", + "translation": "आवश्यक प्रत्यक्ष चैनल संपत्ति गुम है: members" + }, + { + "id": "app.import.validate_direct_channel_import_data.header_length.error", + "translation": "डायरेक्ट चैनल हेडर बहुत लंबा है" + }, + { + "id": "app.import.validate_channel_import_data.type_missing.error", + "translation": "आवश्यक चैनल गुण गुम है: type।" + }, + { + "id": "app.import.validate_channel_import_data.type_invalid.error", + "translation": "चैनल प्रकार अमान्य है |" + }, + { + "id": "app.import.validate_channel_import_data.team_missing.error", + "translation": "आवश्यक चैनल संपत्ति गुम है: team" + }, + { + "id": "api.templates.welcome_subject", + "translation": "[{{ .SiteName }}] आप {{ .ServerURL }} में शामिल हुए" + }, + { + "id": "api.templates.welcome_body.title", + "translation": "टीम में स्वागत है" + }, + { + "id": "api.templates.welcome_body.info", + "translation": "इस ईमेल का उपयोग मैटरमोस्ट के साथ एक अकाऊंत बनाने के लिए किया गया था।" + }, + { + "id": "api.templates.welcome_body.button", + "translation": "ईमेल सत्यापित करें" + }, + { + "id": "api.templates.welcome_body.app_download_info", + "translation": "बेहतरीन अनुभव के लिए, PC, Mac, iOS और Android के लिए ऐप डाउनलोड करें।" + }, + { + "id": "api.templates.warn_metric_ack.subject", + "translation": "मैटरमोस्ट हमसे संपर्क करें अनुरोध" + }, + { + "id": "api.templates.warn_metric_ack.footer", + "translation": "यदि आपके पास कोई अतिरिक्त पूछताछ है, तो कृपया support@mattermost.com पर संपर्क करें" + }, + { + "id": "api.templates.warn_metric_ack.body.site_url_header", + "translation": "साईट URL: " + }, + { + "id": "api.templates.warn_metric_ack.body.registered_users_header", + "translation": "कुल सक्रिय उपयोगकर्ता: " + }, + { + "id": "api.templates.warn_metric_ack.body.diagnostic_id_header", + "translation": "डायग्नोस्टिक आईडी: " + }, + { + "id": "api.templates.warn_metric_ack.body.contact_name_header", + "translation": "संपर्क: " + }, + { + "id": "api.templates.warn_metric_ack.body.contact_email_header", + "translation": "ईमेल: " + }, + { + "id": "api.templates.verify_subject", + "translation": "[{{ .SiteName }}] ईमेल सत्यापन" + }, + { + "id": "api.templates.verify_body.title", + "translation": "अपने ईमेल की पुष्टि करें" + }, + { + "id": "api.templates.verify_body.info", + "translation": "इस ईमेल का उपयोग मैटरमोस्ट के साथ एक अकाऊंत बनाने के लिए किया गया था।" + }, + { + "id": "api.templates.verify_body.button", + "translation": "ईमेल सत्यापित करें" + }, + { + "id": "api.templates.username_change_subject", + "translation": "[{{ .SiteName }}] आपका उपयोगकर्ता नाम बदल गया है" + }, + { + "id": "api.templates.username_change_body.title", + "translation": "आपने अपना उपयोगकर्ता नाम अपडेट किया" + }, + { + "id": "api.templates.username_change_body.info", + "translation": "{{.TeamDisplayName}} के लिए आपका उपयोगकर्ता नाम बदलकर {{.NewUsername}} कर दिया गया है।" + }, + { + "id": "api.templates.user_access_token_subject", + "translation": "[{{ .SiteName }}] व्यक्तिगत पहुंच टोकन आपके खाते में जोड़ा गया" + }, + { + "id": "api.templates.user_access_token_body.title", + "translation": "व्यक्तिगत पहुंच टोकन आपके खाते में जोड़ा गया" + }, + { + "id": "api.templates.user_access_token_body.info", + "translation": "{{ .SiteURL }} पर आपके खाते में एक व्यक्तिगत पहुंच टोकन जोड़ा गया था। उनका उपयोग आपके खाते से {{.SiteName}} तक पहुंचने के लिए किया जा सकता है।" + }, + { + "id": "api.templates.signin_change_email.subject", + "translation": "[{{ .SiteName }}] आपकी साइन-इन विधि अपडेट कर दी गई है" + }, + { + "id": "api.templates.signin_change_email.body.title", + "translation": "आपने अपना साइन-इन तरीका अपडेट किया" + }, + { + "id": "api.templates.signin_change_email.body.method_email", + "translation": "ईमेल और पासवर्ड" + }, + { + "id": "api.templates.signin_change_email.body.info", + "translation": "आपने अपनी साइन-इन पद्धति को {{ .SiteName }} से {{.Method}} में अपडेट कर दिया है।" + }, + { + "id": "api.templates.reset_subject", + "translation": "[{{ .SiteName }}] अपना पासवर्ड रीसेट करें" + }, + { + "id": "api.templates.reset_body.title", + "translation": "अपना पासवर्ड रीसेट करें" + }, + { + "id": "api.templates.reset_body.subTitle", + "translation": "अपना पासवर्ड रीसेट करने के लिए नीचे दिए गए बटन पर क्लिक करें। यदि आपने इसका अनुरोध नहीं किया है, तो आप इस ईमेल को सुरक्षित रूप से अनदेखा कर सकते हैं।" + }, + { + "id": "api.templates.reset_body.info", + "translation": "पासवर्ड रीसेट लिंक 24 घंटों में समाप्त हो जाता है।" + }, + { + "id": "api.templates.reset_body.button", + "translation": "रीसेट पासवर्ड" + }, + { + "id": "api.templates.remove_expired_license.subject", + "translation": "मैटरमोस्ट एंटरप्राइज लाइसेंस अक्षम कर दिया गया है।" + }, + { + "id": "api.templates.remove_expired_license.body.title", + "translation": "आपका एंटरप्राइज़ संस्करण लाइसेंस समाप्त हो गया है और कुछ सुविधाएं अक्षम हो सकती हैं। कृपया अपना लाइसेंस अभी रीन्यू करें।" + }, + { + "id": "api.templates.remove_expired_license.body.renew_button", + "translation": "लाइसेंस अभी रीन्यू करें" + }, + { + "id": "api.templates.post_body.button", + "translation": "मैसेज देखें" + }, + { + "id": "api.templates.password_change_subject", + "translation": "[{{ .SiteName }}] आपका पासवर्ड अपडेट कर दिया गया है" + }, + { + "id": "api.templates.password_change_body.title", + "translation": "आपका पासवर्ड बदला जा चुका है" + }, + { + "id": "api.templates.password_change_body.info", + "translation": "आपका पासवर्ड {{.TeamDisplayName}} के लिए {{.TeamURL}} पर {{.Method}} द्वारा अपडेट कर दिया गया है।" + }, + { + "id": "api.templates.mfa_change_subject", + "translation": "[{{ .SiteName }}] आपका MFA अपडेट कर दिया गया है" + }, + { + "id": "api.templates.license_up_for_renewal_title", + "translation": "आपकी मैटरमोस्ट सदस्यता रीन्यूअल के लिए तैयार है" + }, + { + "id": "api.templates.license_up_for_renewal_subtitle_two", + "translation": "रीन्यू करने के लिए अपने ग्राहक खाते में लॉग इन करें" + }, + { + "id": "api.templates.license_up_for_renewal_subtitle", + "translation": "{{.UserName}}, आपकी सदस्यता {{.Days}} दिनों में समाप्त होने वाली है। हम आशा करते हैं कि आप मैटरमोस्ट द्वारा सक्षम लचीले, सुरक्षित टीम सहयोग का अनुभव कर रहे हैं। यह सुनिश्चित करने के लिए जल्द ही रीन्यू करें कि आपकी टीम इन लाभों का आनंद लेती रहे।" + }, + { + "id": "api.templates.license_up_for_renewal_renew_now", + "translation": "अब रीन्यू करें" + }, + { + "id": "api.templates.license_up_for_renewal_subject", + "translation": "आपका लाइसेंस रीन्यूअल के लिए तैयार है" + }, + { + "id": "api.templates.invite_subject", + "translation": "[{{ .SiteName }}] {{ .SenderName }} ने आपको {{ .TeamDisplayName }} टीम में शामिल होने के लिए आमंत्रित किया है" + }, + { + "id": "api.templates.invite_guest_subject", + "translation": "[{{ .SiteName }}] {{ .SenderName }} ने आपको अतिथि के रूप में {{ .TeamDisplayName }} टीम में शामिल होने के लिए आमंत्रित किया" + }, + { + "id": "api.templates.invite_body_guest.subTitle", + "translation": "टीम के साथ सहयोग करने के लिए आपको अतिथि के रूप में आमंत्रित किया गया था" + }, + { + "id": "api.templates.invite_body_footer.title", + "translation": "मैटरमोस्ट क्या है?" + }, + { + "id": "api.templates.invite_body_footer.learn_more", + "translation": "अधिक जानें" + }, + { + "id": "api.templates.invite_body_footer.info", + "translation": "मैटरमॉस्ट एक लचीला, ओपन सोर्स मैसेजिंग प्लेटफॉर्म है जो सुरक्षित टीम सहयोग को सक्षम बनाता है।" + }, + { + "id": "api.templates.invite_body.title", + "translation": "{{ .SenderName }} ने आपको {{ .TeamDisplayName }} टीम में शामिल होने के लिए आमंत्रित किया है।" + }, + { + "id": "api.templates.invite_body.subTitle", + "translation": "मैटरमोस्ट पर अपनी टीम के साथ सहयोग करना शुरू करें" + }, + { + "id": "api.templates.invite_body.button", + "translation": "अब शामिल हों" + }, + { + "id": "api.templates.email_warning", + "translation": "यदि आपने यह बदलाव नहीं किया है, तो कृपया सिस्टम व्यवस्थापक से संपर्क करें।" + }, + { + "id": "api.templates.email_organization", + "translation": "द्वारा भेजा " + }, + { + "id": "api.templates.email_info3", + "translation": "{{.SiteName}} टीम" + }, + { + "id": "api.templates.email_info2", + "translation": "शुभकामनाएं," + }, + { + "id": "api.templates.email_info1", + "translation": "कोई भी प्रश्न, हमें किसी भी समय मेल करें: " + }, + { + "id": "api.templates.email_change_verify_subject", + "translation": "[{{ .SiteName }}] नया ईमेल पता सत्यापित करें" + }, + { + "id": "api.templates.email_change_verify_body.title", + "translation": "आपने अपना ईमेल अपडेट किया" + }, + { + "id": "api.templates.email_change_verify_body.info", + "translation": "{{.TeamDisplayName}} के लिए अपना ईमेल अपडेट करना समाप्त करने के लिए, कृपया नीचे दिए गए लिंक पर क्लिक करके पुष्टि करें कि यह सही पता है।" + }, + { + "id": "api.templates.email_change_verify_body.button", + "translation": "ईमेल सत्यापित करें" + }, + { + "id": "api.templates.email_change_subject", + "translation": "[{{ .SiteName }}] आपका ईमेल बदल गया है" + }, + { + "id": "api.templates.email_change_body.title", + "translation": "आपने अपना ईमेल अपडेट किया" + }, + { + "id": "api.templates.email_change_body.info", + "translation": "{{.TeamDisplayName}} के लिए आपका ईमेल बदलकर {{.NewEmail}} कर दिया गया है।" + }, + { + "id": "api.templates.cloud_welcome_email.title", + "translation": "{{.WorkSpace}} वर्कस्पेस का आपका १४ दिन का परीक्षण तैयार है!" + }, + { + "id": "api.templates.cloud_welcome_email.subtitle_info", + "translation": "अपनी टीम बनाने और अपने वर्कस्पेस का अधिकतम लाभ उठाने के लिए निम्नलिखित कदम उठाएं।" + }, + { + "id": "api.templates.cloud_welcome_email.subtitle", + "translation": "अपना वर्कस्पेस सेट करें" + }, + { + "id": "api.templates.cloud_welcome_email.subject", + "translation": "बधाई हो!" + }, + { + "id": "api.templates.cloud_welcome_email.start_questions", + "translation": "आरंभ करने के बारे में प्रश्न हैं? हमें ईमेल करें" + }, + { + "id": "api.templates.cloud_welcome_email.signin_sub_info2", + "translation": "PC, Mac, iOS और Android पर सर्वश्रेष्ठ अनुभव के लिए।" + }, + { + "id": "api.reaction.save_reaction.invalid.app_error", + "translation": "प्रतिक्रिया मान्य नहीं है।" + }, + { + "id": "api.reaction.save.archived_channel.app_error", + "translation": "आप आर्काइव्ड चैनल में प्रतिक्रिया नहीं दे सकते।" + }, + { + "id": "api.push_notifications.session.expired", + "translation": "सत्र समाप्त: सूचनाएं प्राप्त करना जारी रखने के लिए कृपया लॉग इन करें। {{.siteName}} के सत्र आपके सिस्टम व्यवस्थापक द्वारा प्रत्येक {{.daysCount}} दिन (दिनों) की अवधि समाप्त होने के लिए कॉन्फ़िगर किए गए हैं।" + }, + { + "id": "api.push_notifications.message.parse.app_error", + "translation": "पुश सूचना मैसेज बनाने में त्रुटि हुई।" + }, + { + "id": "api.push_notification.title.collapsed_threads_dm", + "translation": "सीधे मैसेज में उत्तर दें" + }, + { + "id": "api.push_notification.title.collapsed_threads", + "translation": "{{.channelName}} में जवाब दें" + }, + { + "id": "api.push_notification.id_loaded.fetch.app_error", + "translation": "आईडी-लोडेड पुश सूचना प्राप्त करने में एक त्रुटि हुई।" + }, + { + "id": "api.push_notification.id_loaded.default_message", + "translation": "आपको एक नया मैसेज प्राप्त हुआ है।" + }, + { + "id": "api.push_notification.disabled.app_error", + "translation": "इस सर्वर पर पुश सूचनाएं अक्षम हैं।" + }, + { + "id": "api.post_get_post_by_id.get.app_error", + "translation": "पोस्ट नहीं मिल पा रहा है।" + }, + { + "id": "api.post.update_post.system_message.app_error", + "translation": "सिस्टम मैसेज अपडेट करने में असमर्थ।" + }, + { + "id": "api.post.update_post.permissions_time_limit.app_error", + "translation": "पोस्ट संपादन की अनुमति केवल {{.timeLimit}} सेकंड के लिए है। विवरण के लिए कृपया अपने सिस्टम व्यवस्थापक से पूछें।" + }, + { + "id": "api.post.update_post.permissions_details.app_error", + "translation": "पहले ही हटाई गई id={{.PostId}}." + }, + { + "id": "api.post.update_post.find.app_error", + "translation": "अद्यतन करने के लिए मौजूदा पोस्ट या कमेंट खोजने में असमर्थ।" + }, + { + "id": "api.post.update_post.can_not_update_post_in_deleted.error", + "translation": "हटाए गए चैनल में किसी पोस्ट को अपडेट नहीं किया जा सकता." + }, + { + "id": "api.post.send_notifications_and_forget.push_message", + "translation": "आपको एक मैसेज भेजा।" + }, + { + "id": "api.post.send_notifications_and_forget.push_image_only", + "translation": " एक फाइल संलग्न की।" + }, + { + "id": "api.post.send_notifications_and_forget.push_explicit_mention", + "translation": " आपका वर्णन किया।" + }, + { + "id": "api.post.send_notification_and_forget.push_comment_on_thread", + "translation": " उस थ्रेड पर कमेंट किया जिसमें आपने भाग लिया था।" + }, + { + "id": "api.post.send_notification_and_forget.push_comment_on_post", + "translation": " आपकी पोस्ट पर कमेंट किया।" + }, + { + "id": "api.post.send_notification_and_forget.push_comment_on_crt_thread_dm", + "translation": " थ्रेड का जवाब दिया।" + }, + { + "id": "api.post.send_notification_and_forget.push_channel_mention", + "translation": " चैनल को सूचित किया।" + }, + { + "id": "api.post.search_posts.invalid_body.app_error", + "translation": "रिक्वेस्त बॉडी को पार्स करने में असमर्थ।" + }, + { + "id": "api.post.patch_post.can_not_update_post_in_deleted.error", + "translation": "हटाए गए चैनल में किसी पोस्ट को अपडेट नहीं किया जा सकता." + }, + { + "id": "api.post.do_action.action_id.app_error", + "translation": "अमान्य क्रिया आईडी।" + }, + { + "id": "api.post.disabled_here", + "translation": "@here को अक्षम कर दिया गया है क्योंकि चैनल में {{.Users}} से अधिक उपयोगकर्ता हैं।" + }, + { + "id": "api.post.disabled_channel", + "translation": "@channel को अक्षम कर दिया गया है क्योंकि चैनल में {{.Users}} से अधिक उपयोगकर्ता हैं।" + }, + { + "id": "api.post.disabled_all", + "translation": "@all को अक्षम कर दिया गया है क्योंकि चैनल में {{.Users}} से अधिक उपयोगकर्ता हैं।" + }, + { + "id": "api.post.delete_post.can_not_delete_post_in_deleted.error", + "translation": "हटाए गए चैनल में किसी पोस्ट को हटा नहीं सकते।" + }, + { + "id": "api.oauth.get_access_token.internal_user.app_error", + "translation": "सर्वर त्रुटि: उपयोगकर्ता को डेटाबेस से खींचते समय आंतरिक सर्वर त्रुटि का सामना करना पड़ा।" + }, + { + "id": "api.oauth.get_access_token.internal_session.app_error", + "translation": "server_error: डेटाबेस में सत्र सहेजते समय आंतरिक सर्वर त्रुटि का सामना करना पड़ा।" + }, + { + "id": "api.oauth.get_access_token.internal_saving.app_error", + "translation": "server_error: डेटाबेस तक पहुंच टोकन सहेजते समय आंतरिक सर्वर त्रुटि का सामना करना पड़ा।" + }, + { + "id": "api.oauth.get_access_token.internal.app_error", + "translation": "server_error: डेटाबेस तक पहुँचने के दौरान आंतरिक सर्वर त्रुटि का सामना करना पड़ा।" + }, + { + "id": "api.oauth.get_access_token.expired_code.app_error", + "translation": "invalid_grant: अमान्य या समाप्त प्राधिकरण कोड।" + }, + { + "id": "api.oauth.get_access_token.disabled.app_error", + "translation": "सिस्टम व्यवस्थापक ने OAuth2 सेवा प्रदाता को बंद कर दिया है।" + }, + { + "id": "api.admin.saml.set_certificate_from_metadata.invalid_content_type.app_error", + "translation": "अमान्य सामग्री प्रकार।" + }, + { + "id": "api.admin.saml.set_certificate_from_metadata.invalid_body.app_error", + "translation": "अमान्य प्रमाणपत्र टेक्स्ट." + }, + { + "id": "api.admin.saml.not_available.app_error", + "translation": "SAML 2.0 इस सर्वर पर कॉन्फ़िगर या समर्थित नहीं है।" + }, + { + "id": "api.channel.channel_member_counts_by_group.license.error", + "translation": "आपका लाइसेंस समूहों का समर्थन नहीं करता" + }, + { + "id": "api.channel.change_channel_privacy.public_to_private", + "translation": "इस चैनल को एक निजी चैनल में बदल दिया गया है।" + }, + { + "id": "api.channel.change_channel_privacy.private_to_public", + "translation": "इस चैनल को एक सार्वजनिक चैनल में बदल दिया गया है और इसमें टीम का कोई भी सदस्य शामिल हो सकता है।" + }, + { + "id": "api.channel.add_user_to_channel.type.app_error", + "translation": "उपयोगकर्ता को इस चैनल प्रकार में नहीं जोड़ा जा सकता." + }, + { + "id": "api.channel.add_user.to.channel.failed.deleted.app_error", + "translation": "उपयोगकर्ता को चैनल में जोड़ने में विफल क्योंकि उन्हें टीम से निकाल दिया गया है।" + }, + { + "id": "api.channel.add_user.to.channel.failed.app_error", + "translation": "उपयोगकर्ता को चैनल में जोड़ने में विफल." + }, + { + "id": "api.channel.add_members.user_denied", + "translation": "समूह की कमी के कारण निम्नलिखित उपयोगकर्ताओं को चैनल सदस्यता अस्वीकृत कर दी गई: {{ .UserIDs }}" + }, + { + "id": "api.channel.add_members.error", + "translation": "चैनल सदस्यों को जोड़ने में त्रुटि।" + }, + { + "id": "api.channel.add_member.added", + "translation": "%v चैनल में %v द्वारा जोड़ा गया।" + }, + { + "id": "api.channel.add_guest.added", + "translation": "%v ने चैनल में %v द्वारा अतिथि के रूप में जोड़ा।" + }, + { + "id": "api.bot.teams_channels.add_message_mobile", + "translation": "कृपया मुझे उन टीमों और चैनलों में जोड़ें जिनमें आप चाहते हैं कि मैं बातचीत करूं। ऐसा करने के लिए, ब्राउज़र या मैटरमोस्ट डेस्कटॉप ऐप का उपयोग करें।" + }, + { + "id": "api.bot.create_disabled", + "translation": "बॉट निर्माण अक्षम कर दिया गया है।" + }, + { + "id": "api.back_to_app", + "translation": "{{.SiteName}} पर वापस जाएं" + }, + { + "id": "api.admin.upload_brand_image.too_large.app_error", + "translation": "फाइल अपलोड करने में असमर्थ। फ़ाइल बहुत बड़ी है।" + }, + { + "id": "api.admin.upload_brand_image.parse.app_error", + "translation": "मल्टीपार्ट फॉर्म को पार्स नहीं किया जा सका।" + }, + { + "id": "api.admin.test_s3.missing_s3_bucket", + "translation": "S3 बकेट आवश्यक है" + }, + { + "id": "api.admin.test_email.subject", + "translation": "मैटरमोस्ट - ईमेल सेटिंग्स का परीक्षण" + }, + { + "id": "api.admin.test_email.reenter_password", + "translation": "SMTP सर्वर, पोर्ट या उपयोगकर्ता नाम बदल गया है। कनेक्शन का परीक्षण करने के लिए कृपया SMTP पासवर्ड दोबारा दर्ज करें।" + }, + { + "id": "api.admin.test_email.missing_server", + "translation": "SMTP सर्वर की आवश्यकता है" + }, + { + "id": "api.admin.test_email.body", + "translation": "ऐसा लगता है कि आपका मैटरमोस्ट ईमेल सही तरीके से सेटअप है!" + }, + { + "id": "api.channel.update_channel_scheme.license.error", + "translation": "आपका लाइसेंस किसी चैनल की योजना को अपडेट करने का समर्थन नहीं करता" + }, + { + "id": "api.channel.update_channel_privacy.default_channel_error", + "translation": "डिफ़ॉल्ट चैनल को निजी नहीं बनाया जा सकता." + }, + { + "id": "api.channel.update_channel_member_roles.scheme_role.app_error", + "translation": "प्रदान की गई भूमिका एक योजना द्वारा प्रबंधित की जाती है और इसलिए इसे सीधे चैनल सदस्य पर लागू नहीं किया जा सकता है।" + }, + { + "id": "api.channel.update_channel_member_roles.changing_guest_role.app_error", + "translation": "अमान्य चैनल सदस्य अद्यतन: आप अतिथि भूमिका को मैन्युअल रूप से जोड़ या हटा नहीं सकते हैं।" + }, + { + "id": "api.channel.update_channel.typechange.app_error", + "translation": "चैनल प्रकार को अपडेट नहीं किया जा सकता है।" + }, + { + "id": "api.channel.update_channel.tried.app_error", + "translation": "डिफ़ॉल्ट चैनल {{.Channel}} का अमान्य अद्यतन करने का प्रयास किया।" + }, + { + "id": "api.channel.update_channel.deleted.app_error", + "translation": "चैनल को संग्रहीत या हटा दिया गया है।" + }, + { + "id": "api.channel.restore_channel.restored.app_error", + "translation": "चैनल को संग्रह से निकालने में असमर्थ. चैनल संग्रहीत नहीं है।" + }, + { + "id": "api.channel.remove_user_from_channel.app_error", + "translation": "उपयोगकर्ता को इस प्रकार के चैनल से नहीं निकाला जा सकता." + }, + { + "id": "api.channel.remove_members.denied", + "translation": "समूह की कमी के कारण निम्नलिखित उपयोगकर्ताओं को चैनल सदस्यता निष्कासन अस्वीकार कर दिया गया: {{ .UserIDs }}" + }, + { + "id": "api.channel.remove_member.removed", + "translation": "%v को चैनल से हटाया गया।" + }, + { + "id": "api.channel.remove_member.group_constrained.app_error", + "translation": "किसी उपयोगकर्ता को समूह-विवश चैनल से निकालने में असमर्थ।" + }, + { + "id": "api.channel.remove_channel_member.type.app_error", + "translation": "किसी चैनल से उपयोगकर्ता को निकालने में असमर्थ." + }, + { + "id": "api.channel.remove.default.app_error", + "translation": "उपयोगकर्ता को डिफ़ॉल्ट चैनल {{.Channel}} से निकालने में असमर्थ।" + }, + { + "id": "api.channel.post_update_channel_header_message_and_forget.updated_to", + "translation": "%s ने चैनल हैडर को इसमें अपडेट किया: %s" + }, + { + "id": "api.channel.post_update_channel_header_message_and_forget.updated_from", + "translation": "%s ने चैनल हैडर को यहां से अपडेट किया: %s से: %s" + }, + { + "id": "api.channel.post_update_channel_header_message_and_forget.retrieve_user.error", + "translation": "चैनल हेडर अपडेट करते समय उपयोगकर्ता को पुनः प्राप्त करने में विफल" + }, + { + "id": "api.channel.post_update_channel_header_message_and_forget.removed", + "translation": "%s ने चैनल हैडर हटा दिया (था: %s)" + }, + { + "id": "api.channel.post_update_channel_displayname_message_and_forget.updated_from", + "translation": "%s ने चैनल प्रदर्शन नाम को इस से अपडेट किया: %s से: %s" + }, + { + "id": "api.channel.post_update_channel_displayname_message_and_forget.retrieve_user.error", + "translation": "चैनल डिस्प्लेनाम फ़ील्ड को अपडेट करते समय उपयोगकर्ता को पुनः प्राप्त करने में विफल" + }, + { + "id": "api.channel.post_update_channel_displayname_message_and_forget.create_post.error", + "translation": "डिस्प्लेनाम अपडेट मैसेज पोस्ट करने में विफल" + }, + { + "id": "api.channel.patch_update_channel.forbidden.app_error", + "translation": "चैनल को अपडेट करने में विफल." + }, + { + "id": "api.channel.patch_channel_moderations.license.error", + "translation": "आपका लाइसेंस चैनल मॉडरेशन का समर्थन नहीं करता" + }, + { + "id": "api.channel.patch_channel_moderations.cache_invalidation.error", + "translation": "कैशे को अमान्य करने में त्रुटि" + }, + { + "id": "api.channel.leave.left", + "translation": "%v ने चैनल छोड़ दिया।" + }, + { + "id": "api.channel.leave.last_member.app_error", + "translation": "आप अकेले सदस्य हैं, छोड़ने के बजाय निजी चैनल को निकालने का प्रयास करें।" + }, + { + "id": "api.command_code.name", + "translation": "कोड" + }, + { + "id": "api.command_code.message.app_error", + "translation": "/code कमांड के साथ एक मैसेज अवश्य दिया जाना चाहिए।" + }, + { + "id": "api.command_code.hint", + "translation": "[text]" + }, + { + "id": "api.command_code.desc", + "translation": "टेक्स्ट को कोड ब्लॉक के रूप में प्रदर्शित करें" + }, + { + "id": "api.command_channel_rename.update_channel.app_error", + "translation": "वर्तमान चैनल को अपडेट करने में त्रुटि।" + }, + { + "id": "api.command_channel_rename.too_short.app_error", + "translation": "चैनल का नाम {{.Length}} या अधिक वर्णों का होना चाहिए।" + }, + { + "id": "api.command_channel_rename.too_long.app_error", + "translation": "चैनल का नाम {{.Length}} या उससे कम वर्णों का होना चाहिए।" + }, + { + "id": "api.command_channel_rename.permission.app_error", + "translation": "आपके पास चैनल का नाम बदलने के लिए उपयुक्त अनुमति नहीं है।" + }, + { + "id": "api.command_channel_rename.name", + "translation": "नाम बदलने" + }, + { + "id": "api.command_channel_rename.message.app_error", + "translation": "/rename कमांड के साथ एक मैसेज अवश्य दिया जाना चाहिए।" + }, + { + "id": "api.command_channel_rename.hint", + "translation": "[text]" + }, + { + "id": "api.command_channel_rename.direct_group.app_error", + "translation": "सीधे संदेश चैनलों का नाम बदलने में असमर्थ।" + }, + { + "id": "api.server.warn_metric.number_of_posts_2M.notification_title", + "translation": "प्रदर्शन सुधारना" + }, + { + "id": "api.server.warn_metric.number_of_channels_50.contact_us.email_body", + "translation": "सबसे महत्वपूर्ण हमसे संपर्क करें अनुरोध। मुझे सिस्टम योजनाओं के साथ उन्नत अनुमतियों का उपयोग करने के बारे में अधिक जानने में दिलचस्पी है।\n" + }, + { + "id": "api.server.warn_metric.number_of_active_users_500.start_trial.notification_success.message", + "translation": "आपका एंटरप्राइज़ परीक्षण अब सक्रिय है। उन्नत सुविधाओं को सक्षम करने के लिए सिस्टम कंसोल पर जाएं।" + }, + { + "id": "api.server.warn_metric.number_of_active_users_500.notification_title", + "translation": "मैटरमोस्ट के साथ स्केलिंग" + }, + { + "id": "api.server.warn_metric.number_of_active_users_500.contact_us.email_body", + "translation": "सबसे महत्वपूर्ण हमसे संपर्क करें अनुरोध। मेरी टीम में अब 500 उपयोगकर्ता हैं, और मैं मैटरमोस्ट एंटरप्राइज संस्करण पर विचार कर रहा हूं।\n" + }, + { + "id": "api.server.warn_metric.number_of_active_users_300.start_trial.notification_title", + "translation": "केवल पढ़ने के लिए घोषणा चैनल" + }, + { + "id": "api.server.warn_metric.number_of_active_users_300.start_trial.notification_success.message", + "translation": "आपका एंटरप्राइज़ परीक्षण अब सक्रिय है। चैनल बनाएं और चैनल व्यवस्थापकों तक पोस्टिंग को सीमित करने के लिए **System Console > User Management > Channels** पर जाएं।" + }, + { + "id": "api.server.warn_metric.number_of_active_users_300.contact_us.email_body", + "translation": "सबसे महत्वपूर्ण हमसे संपर्क करें अनुरोध। मुझे केवल-पढ़ने के लिए घोषणा चैनल बनाने के बारे में और जानने में दिलचस्पी है।\n" + }, + { + "id": "api.server.warn_metric.number_of_active_users_200.notification_title", + "translation": "मैटरमोस्ट के साथ स्केलिंग" + }, + { + "id": "api.server.warn_metric.number_of_active_users_200.contact_us.email_body", + "translation": "सबसे महत्वपूर्ण हमसे संपर्क करें अनुरोध। मेरी टीम में अब 200 उपयोगकर्ता हैं, और मैं मैटरमोस्ट एंटरप्राइज संस्करण पर विचार कर रहा हूं।\n" + }, + { + "id": "api.server.warn_metric.number_of_active_users_100.notification_title", + "translation": "मैटरमोस्ट के साथ स्केलिंग" + }, + { + "id": "api.server.warn_metric.number_of_active_users_100.contact_us.email_body", + "translation": "सबसे महत्वपूर्ण हमसे संपर्क करें अनुरोध। मेरी टीम में अब 100 उपयोगकर्ता हैं, और मैं मैटरमोस्ट एंटरप्राइज संस्करण पर विचार कर रहा हूं।\n" + }, + { + "id": "api.command_channel_rename.desc", + "translation": "चैनल का नाम बदलें" + }, + { + "id": "api.command_channel_rename.channel.app_error", + "translation": "वर्तमान चैनल को पुनः प्राप्त करने में त्रुटि।" + }, + { + "id": "api.command_channel_remove.channel.app_error", + "translation": "वर्तमान चैनल को पुनः प्राप्त करने में त्रुटि।" + }, + { + "id": "api.command_channel_purpose.update_channel.max_length", + "translation": "दर्ज किया गया टेक्स्ट वर्ण सीमा से अधिक है। चैनल का उद्देश्य {{.MaxLength}} वर्णों तक सीमित है।" + }, + { + "id": "api.command_channel_purpose.update_channel.app_error", + "translation": "चैनल के उद्देश्य को अपडेट करने में त्रुटि।" + }, + { + "id": "api.command_channel_purpose.permission.app_error", + "translation": "आपके पास चैनल के उद्देश्य को संपादित करने के लिए उपयुक्त अनुमतियां नहीं हैं।" + }, + { + "id": "api.command_channel_purpose.name", + "translation": "उद्देश्य" + }, + { + "id": "api.command_channel_purpose.message.app_error", + "translation": "/purpose कमांड के साथ एक मैसेज प्रदान किया जाना चाहिए।" + }, + { + "id": "api.command_channel_purpose.hint", + "translation": "[text]" + }, + { + "id": "api.command_channel_purpose.direct_group.app_error", + "translation": "सीधे संदेश चैनलों के लिए उद्देश्य निर्धारित करने में असमर्थ। इसके बजाय हेडर सेट करने के लिए /header का उपयोग करें।" + }, + { + "id": "api.command_channel_purpose.desc", + "translation": "चैनल का उद्देश्य संपादित करें" + }, + { + "id": "api.command_channel_purpose.channel.app_error", + "translation": "वर्तमान चैनल को पुनः प्राप्त करने में त्रुटि।" + }, + { + "id": "api.command_channel_header.update_channel.max_length", + "translation": "दर्ज किया गया टेक्स्ट वर्ण सीमा से अधिक है। चैनल हेडर {{.MaxLength}} वर्णों तक सीमित है।" + }, + { + "id": "api.command_channel_header.update_channel.app_error", + "translation": "चैनल हेडर को अपडेट करने में त्रुटि।" + }, + { + "id": "api.command_channel_header.permission.app_error", + "translation": "आपके पास चैनल हेडर संपादित करने के लिए उपयुक्त अनुमतियां नहीं हैं।" + }, + { + "id": "api.command_channel_header.name", + "translation": "हैडर" + }, + { + "id": "api.command_channel_header.message.app_error", + "translation": "टेक्स्ट को /header कमांड के साथ प्रदान किया जाना चाहिए।" + }, + { + "id": "api.command_channel_header.hint", + "translation": "[text]" + }, + { + "id": "api.command_channel_header.desc", + "translation": "चैनल हेडर संपादित करें" + }, + { + "id": "api.command_channel_header.channel.app_error", + "translation": "वर्तमान चैनल को पुनः प्राप्त करने में त्रुटि।" + }, + { + "id": "api.command_away.success", + "translation": "अब आप दूर हैं" + }, + { + "id": "api.command_away.name", + "translation": "अवे" + }, + { + "id": "api.command_away.desc", + "translation": "अपना स्तेतस अवे सेट करें" + }, + { + "id": "api.command.team_mismatch.app_error", + "translation": "टीमों में कमांड अपडेट करने में असमर्थ।" + }, + { + "id": "api.command.invite_people.sent", + "translation": "ईमेल आमंत्रण भेजा गया" + }, + { + "id": "api.command.invite_people.no_email", + "translation": "कृपया एक या अधिक मान्य ईमेल निर्दिष्ट करें" + }, + { + "id": "api.command.invite_people.name", + "translation": "invite_people" + }, + { + "id": "api.command.invite_people.invite_off", + "translation": "इस सर्वर पर उपयोगकर्ता निर्माण अक्षम कर दिया गया है, कोई आमंत्रण नहीं भेजा गया" + }, + { + "id": "api.command.invite_people.hint", + "translation": "[name@domain.com ...]" + }, + { + "id": "api.command.invite_people.fail", + "translation": "ईमेल आमंत्रण भेजने में त्रुटि आई" + }, + { + "id": "api.command.invite_people.email_off", + "translation": "ईमेल कॉन्फ़िगर नहीं किया गया है, कोई आमंत्रण नहीं भेजा गया है" + }, + { + "id": "api.command.invite_people.email_invitations_off", + "translation": "ईमेल आमंत्रण अक्षम हैं, कोई आमंत्रण नहीं भेजा गया है" + }, + { + "id": "api.command.invite_people.desc", + "translation": "अपनी मैटरमोस्ट टीम को एक ईमेल आमंत्रण भेजें" + }, + { + "id": "api.command.execute_command.start.app_error", + "translation": "कोई कमांड ट्रिगर नहीं मिला।" + }, + { + "id": "api.command.execute_command.not_found.app_error", + "translation": "'{{.Trigger}}' के ट्रिगर वाला कमांड नहीं मिला। \"/\" से शुरू होने वाला संदेश भेजने के लिए, संदेश की शुरुआत में एक खाली जगह जोड़ने का प्रयास करें।" + }, + { + "id": "api.command.execute_command.format.app_error", + "translation": "कमांड ट्रिगर शब्द में प्रमुख स्लैश वर्ण गुम है" + }, + { + "id": "api.command.execute_command.failed_resp.app_error", + "translation": "'{{.Trigger}}' के ट्रिगर के साथ कमांड ने प्रतिक्रिया दी {{.Status}}।" + }, + { + "id": "api.command.execute_command.failed_empty.app_error", + "translation": "'{{.Trigger}}' के ट्रिगर के साथ कमांड ने एक खाली प्रतिक्रिया दी।" + }, + { + "id": "api.command.execute_command.failed.app_error", + "translation": "'{{.Trigger}}' के ट्रिगर वाला कमांड विफल रहा।" + }, + { + "id": "api.command.execute_command.create_post_failed.app_error", + "translation": "कमांड '{{.Trigger}}' प्रतिक्रिया पोस्ट करने में विफल रहा। कृपया अपने सिस्टम व्यवस्थापक से संपर्क करें।" + }, + { + "id": "api.command.duplicate_trigger.app_error", + "translation": "यह ट्रिगर शब्द पहले से ही प्रयोग में है। कृपया कोई दूसरा शब्द चुनें।" + }, + { + "id": "api.command.disabled.app_error", + "translation": "सिस्टम एडमिन द्वारा कमांड्स को डिसेबल कर दिया गया है।" + }, + { + "id": "api.server.warn_metric.number_of_posts_2M.contact_us.email_body", + "translation": "सबसे महत्वपूर्ण हमसे संपर्क करें अनुरोध। मुझे इलास्टिक्स खोज के साथ प्रदर्शन में सुधार के बारे में और जानने में दिलचस्पी है।\n" + }, + { + "id": "api.server.warn_metric.number_of_channels_50.start_trial.notification_success.message", + "translation": "आपका एंटरप्राइज़ परीक्षण अब सक्रिय है। उन्नत अनुमतियां सक्षम करने के लिए **System Console > User Management > Permissions** पर जाएं।" + }, + { + "id": "api.server.warn_metric.number_of_channels_50.notification_title", + "translation": "उन्नत अनुमतियों का उपयोग करना" + }, + { + "id": "model.team.is_valid.company.app_error", + "translation": "अमान्य कंपनी का नाम." + }, + { + "id": "model.team.is_valid.characters.app_error", + "translation": "नाम 2 या अधिक लोअरकेस अक्षरांकीय वर्ण होना चाहिए।" + }, + { + "id": "model.reaction.is_valid.user_id.app_error", + "translation": "अमान्य उपयोगकर्ता आइ डी।" + }, + { + "id": "model.reaction.is_valid.post_id.app_error", + "translation": "अमान्य पोस्ट आईडी." + }, + { + "id": "model.reaction.is_valid.emoji_name.app_error", + "translation": "अमान्य इमोजी नाम." + }, + { + "id": "model.preference.is_valid.name.app_error", + "translation": "अमान्य नाम।" + }, + { + "id": "model.preference.is_valid.id.app_error", + "translation": "अमान्य उपयोगकर्ता आइ डी।" + }, + { + "id": "model.post.is_valid.user_id.app_error", + "translation": "अमान्य उपयोगकर्ता आइ डी।" + }, + { + "id": "model.post.is_valid.type.app_error", + "translation": "अमान्य प्रकार।" + }, + { + "id": "model.post.is_valid.msg.app_error", + "translation": "अमान्य मैसेज।" + }, + { + "id": "model.post.is_valid.hashtags.app_error", + "translation": "अमान्य हैशटैग।" + }, + { + "id": "model.post.is_valid.filenames.app_error", + "translation": "अमान्य फ़ाइल नाम।" + }, + { + "id": "model.post.is_valid.file_ids.app_error", + "translation": "अमान्य फ़ाइल आईडी. ध्यान दें कि अपलोड अधिकतम 10 फ़ाइलों तक सीमित हैं। कृपया अधिक फ़ाइलों के लिए अतिरिक्त पोस्ट का उपयोग करें।" + }, + { + "id": "model.post.is_valid.channel_id.app_error", + "translation": "अमान्य चैनल आईडी." + }, + { + "id": "model.post.channel_notifications_disabled_in_channel.message", + "translation": "चैनल सूचनाएं {{.ChannelName}} में अक्षम हैं। {{.Mention}} ने कोई सूचना ट्रिगर नहीं की।" + }, + { + "id": "model.plugin_key_value.is_valid.plugin_id.app_error", + "translation": "अमान्य प्लग इन आईडी, {{.Min}} से अधिक और अधिकतम {{.Max}} वर्णों से अधिक लंबा होना चाहिए।" + }, + { + "id": "model.plugin_key_value.is_valid.key.app_error", + "translation": "अमान्य की, {{.Min}} से अधिक और अधिकतम {{.Max}} वर्णों से अधिक लंबी होनी चाहिए।" + }, + { + "id": "model.plugin_command_error.error.app_error", + "translation": "/{{.Command}} के लिए प्लगइन काम नहीं कर रहा है। कृपया अपने सिस्टम व्यवस्थापक से संपर्क करें" + }, + { + "id": "model.plugin_command_crash.error.app_error", + "translation": "/{{.Command}} कमांड ने {{.PluginId}} प्लगइन को क्रैश कर दिया। कृपया अपने सिस्टम व्यवस्थापक से संपर्क करें" + }, + { + "id": "model.plugin_command.error.app_error", + "translation": "इस कमांड को निष्पादित करने का प्रयास करते समय एक त्रुटि उत्पन्न हुई।" + }, + { + "id": "model.outgoing_hook.username.app_error", + "translation": "अमान्य उपयोगकर्ता नाम।" + }, + { + "id": "model.outgoing_hook.is_valid.words.app_error", + "translation": "अमान्य ट्रिगर शब्द." + }, + { + "id": "model.outgoing_hook.is_valid.user_id.app_error", + "translation": "अमान्य उपयोगकर्ता आइ डी।" + }, + { + "id": "model.outgoing_hook.is_valid.url.app_error", + "translation": "अमान्य कॉलबैक URL. प्रत्येक एक मान्य URL होना चाहिए और http:// या https:// से प्रारंभ होना चाहिए।" + }, + { + "id": "model.outgoing_hook.is_valid.trigger_words.app_error", + "translation": "अमान्य ट्रिगर शब्द." + }, + { + "id": "model.outgoing_hook.is_valid.token.app_error", + "translation": "अमान्य टोकन।" + }, + { + "id": "app.user.store_is_empty.app_error", + "translation": "यह जांचने में विफल रहा कि उपयोगकर्ता स्टोर खाली है या नहीं।" + }, + { + "id": "model.user.is_valid.auth_data_pwd.app_error", + "translation": "अमान्य उपयोगकर्ता, पासवर्ड और प्रमाणीकरण डेटा सेट नहीं किए जा सकते।" + }, + { + "id": "model.user.is_valid.auth_data.app_error", + "translation": "अमान्य प्रमाणीकरण डेटा।" + }, + { + "id": "model.upload_session.is_valid.user_id.app_error", + "translation": "UserId के लिए अमान्य मान" + }, + { + "id": "model.upload_session.is_valid.type.app_error", + "translation": "Type के लिए अमान्य मान" + }, + { + "id": "model.upload_session.is_valid.path.app_error", + "translation": "Path के लिए अमान्य मान" + }, + { + "id": "model.upload_session.is_valid.id.app_error", + "translation": "Id के लिए अमान्य मान" + }, + { + "id": "model.upload_session.is_valid.filename.app_error", + "translation": "Filename के लिए अमान्य मान" + }, + { + "id": "model.upload_session.is_valid.file_size.app_error", + "translation": "FileSize के लिए अमान्य मान" + }, + { + "id": "model.upload_session.is_valid.file_offset.app_error", + "translation": "FileOffset के लिए अमान्य मान" + }, + { + "id": "model.upload_session.is_valid.create_at.app_error", + "translation": "CreateAt के लिए अमान्य मान" + }, + { + "id": "model.upload_session.is_valid.channel_id.app_error", + "translation": "ChannelId के लिए अमान्य मान।" + }, + { + "id": "model.token.is_valid.size", + "translation": "अमान्य टोकन।" + }, + { + "id": "model.token.is_valid.expiry", + "translation": "अमान्य टोकन समाप्ति" + }, + { + "id": "model.team_member.is_valid.user_id.app_error", + "translation": "अमान्य उपयोगकर्ता आइ डी।" + }, + { + "id": "model.team_member.is_valid.team_id.app_error", + "translation": "अमान्य टीम आईडी।" + }, + { + "id": "model.team_member.is_valid.roles_limit.app_error", + "translation": "अमान्य टीम सदस्य भूमिकाएं {{.Limit}} वर्णों से अधिक लंबी हैं।" + }, + { + "id": "model.team.is_valid.url.app_error", + "translation": "अमान्य URL पहचानकर्ता।" + }, + { + "id": "model.team.is_valid.type.app_error", + "translation": "अमान्य प्रकार।" + }, + { + "id": "model.team.is_valid.reserved.app_error", + "translation": "यह URL अनुपलब्ध है। कृपया दूसरा प्रयास करें।" + }, + { + "id": "model.team.is_valid.name.app_error", + "translation": "अमान्य नाम।" + }, + { + "id": "model.team.is_valid.invite_id.app_error", + "translation": "अमान्य आमंत्रण आईडी." + }, + { + "id": "model.team.is_valid.email.app_error", + "translation": "अवैध ईमेल।" + }, + { + "id": "model.team.is_valid.domains.app_error", + "translation": "अमान्य अनुमत डोमेन." + }, + { + "id": "ent.elasticsearch.indexer.do_job.get_oldest_entity.error", + "translation": "सबसे पुरानी इकाई (उपयोगकर्ता, चैनल या पोस्ट), डेटाबेस से पुनर्प्राप्त नहीं की जा सकी" + }, + { + "id": "ent.elasticsearch.index_user.error", + "translation": "उपयोगकर्ता को अनुक्रमित करने में विफल" + }, + { + "id": "ent.elasticsearch.index_post.error", + "translation": "पोस्ट को अनुक्रमित करने में विफल" + }, + { + "id": "ent.elasticsearch.index_channel.error", + "translation": "चैनल को अनुक्रमित करने में विफल" + }, + { + "id": "ent.elasticsearch.delete_user_posts.error", + "translation": "उपयोगकर्ता पोस्ट हटाने में विफल" + }, + { + "id": "ent.elasticsearch.delete_user.error", + "translation": "उपयोगकर्ता को हटाने में विफल" + }, + { + "id": "ent.elasticsearch.delete_post.error", + "translation": "पोस्ट को हटाने में विफल" + }, + { + "id": "app.schemes.is_phase_2_migration_completed.not_completed.app_error", + "translation": "यह API एंडपॉइंट पहुंच योग्य नहीं है क्योंकि आवश्यक माइग्रेशन अभी तक पूरा नहीं हुआ है।" + }, + { + "id": "app.scheme.save.invalid_scheme.app_error", + "translation": "प्रदान की गई स्कीम अमान्य है।" + }, + { + "id": "app.scheme.save.app_error", + "translation": "स्कीम बनाने में असमर्थ." + }, + { + "id": "app.scheme.permanent_delete_all.app_error", + "translation": "हम स्कीमों को स्थायी रूप से हटा नहीं सके।" + }, + { + "id": "app.scheme.get.app_error", + "translation": "स्कीम नहीं मिल पा रही है।" + }, + { + "id": "app.scheme.delete.app_error", + "translation": "इस स्कीम को हटाने में असमर्थ." + }, + { + "id": "app.save_config.app_error", + "translation": "कॉन्फ़िगरेशन सेव करते समय एक त्रुटि हुई।" + }, + { + "id": "app.role.save.invalid_role.app_error", + "translation": "भूमिका मान्य नहीं थी।" + }, + { + "id": "app.role.save.insert.app_error", + "translation": "नई भूमिका सेव करने में असमर्थ." + }, + { + "id": "app.role.permanent_delete_all.app_error", + "translation": "हम सभी भूमिकाओं को स्थायी रूप से नहीं हटा सके।" + }, + { + "id": "app.role.get_by_names.app_error", + "translation": "भूमिका नहीं मिल पा रही है।" + }, + { + "id": "app.role.get_by_name.app_error", + "translation": "भूमिका नहीं मिल पा रही है।" + }, + { + "id": "app.role.get.app_error", + "translation": "भूमिका नहीं मिल पा रही है।" + }, + { + "id": "app.role.check_roles_exist.role_not_found", + "translation": "प्रदान की गई भूमिका मौजूद नहीं है" + }, + { + "id": "app.recover.save.app_error", + "translation": "टोकन सेव करने में असमर्थ." + }, + { + "id": "app.recover.delete.app_error", + "translation": "टोकन हटाने में असमर्थ." + }, + { + "id": "app.reaction.get_for_post.app_error", + "translation": "पोस्ट के लिए प्रतिक्रिया प्राप्त करने में असमर्थ।" + }, + { + "id": "app.reaction.delete_all_with_emoji_name.get_reactions.app_error", + "translation": "इस इमोजी नाम के साथ सभी प्रतिक्रियाएं प्राप्त करने में असमर्थ।" + }, + { + "id": "app.reaction.bulk_get_for_post_ids.app_error", + "translation": "पोस्ट के लिए प्रतिक्रिया प्राप्त करने में असमर्थ।" + }, + { + "id": "app.post.update.app_error", + "translation": "पोस्ट अपडेट करने में असमर्थ." + }, + { + "id": "app.post.search.app_error", + "translation": "पोस्ट खोजने में त्रुटि" + }, + { + "id": "app.post.save.existing.app_error", + "translation": "आप किसी मौजूदा पोस्ट को अपडेट नहीं कर सकते।" + }, + { + "id": "app.post.save.app_error", + "translation": "पोस्ट सेव करने में असमर्थ." + }, + { + "id": "app.post.permanent_delete_by_channel.app_error", + "translation": "चैनल द्वारा पोस्ट हटाने में असमर्थ." + }, + { + "id": "app.post.marshal.app_error", + "translation": "पोस्ट को मार्शल करने में विफल।" + }, + { + "id": "app.post.get_root_posts.app_error", + "translation": "चैनल के लिए पोस्ट प्राप्त करने में असमर्थ." + }, + { + "id": "app.post.get_posts_since.app_error", + "translation": "चैनल के लिए पोस्ट प्राप्त करने में असमर्थ." + }, + { + "id": "app.post.get_posts_created_at.app_error", + "translation": "चैनल के लिए पोस्ट प्राप्त करने में असमर्थ." + }, + { + "id": "app.post.get_posts_around.get.app_error", + "translation": "चैनल के लिए पोस्ट प्राप्त करने में असमर्थ." + }, + { + "id": "api.command_share.shared_channel_unavailable", + "translation": "यह चैनल अब शेयर नहीं किया जाता है।" + }, + { + "id": "api.command_share.share_read_only.help", + "translation": "चैनल केवल-पढ़ने के लिए मोड में साझा किया जाएगा" + }, + { + "id": "api.command_share.share_channel.error", + "translation": "इस चैनल को साझा नहीं कर सकता: {{.Error}}" + }, + { + "id": "api.command_share.service_disabled", + "translation": "साझा चैनल सेवा अक्षम है।" + }, + { + "id": "api.command_share.remote_uninvited", + "translation": "सुरक्षित कनेक्शन `{{.RemoteId}}` बिन बुलाए।" + }, + { + "id": "api.command_search.unsupported.app_error", + "translation": "आपके डिवाइस पर खोज कमांड समर्थित नहीं है।" + }, + { + "id": "model.preference.is_valid.theme.app_error", + "translation": "अमान्य थीम." + }, + { + "id": "model.preference.is_valid.category.app_error", + "translation": "अमान्य श्रेणी।" + }, + { + "id": "api.command_search.name", + "translation": "खोजे" + }, + { + "id": "api.command_search.desc", + "translation": "मैसेजो में टेक्स्ट खोजें" + }, + { + "id": "api.command_remove.user_not_in_channel", + "translation": "{{.Username}} इस चैनल का सदस्य नहीं है।" + }, + { + "id": "api.command_remove.permission.app_error", + "translation": "आपके पास सदस्य को निकालने के लिए उपयुक्त अनुमति नहीं है।" + }, + { + "id": "api.command_remove.missing.app_error", + "translation": "हमें उपयोगकर्ता नहीं मिला. हो सकता है कि उन्हें सिस्टम प्रशासक द्वारा निष्क्रिय कर दिया गया हो।" + }, + { + "id": "api.command_remove.message.app_error", + "translation": "/remove या /kick कमांड के साथ एक मैसेज अवश्य दिया जाना चाहिए।" + }, + { + "id": "api.command_remove.group_constrained_user_denied", + "translation": "उपयोगकर्ता को आपके द्वारा चैनल से हटाया नहीं जा सकता क्योंकि वे इस चैनल से जुड़े समूहों के सदस्य हैं। उन्हें इस चैनल से हटाने के लिए, उन्हें लिंक किए गए समूहों से हटाना होगा।" + }, + { + "id": "api.command_remove.desc", + "translation": "किसी सदस्य को चैनल से निकाले" + }, + { + "id": "api.command_remote.unknown_action", + "translation": "अज्ञात क्रिया `{{.Action}}`" + }, + { + "id": "api.command_remote.status.help", + "translation": "सभी सुरक्षित कनेक्शन के लिए स्तेतस प्रदर्शित करता है" + }, + { + "id": "api.command_remote.site_url_not_set", + "translation": "साइट URL सेट नहीं है। कृपया इसे System Console > Environment > Web Server में सेट करें।" + }, + { + "id": "api.command_remote.service_not_enabled", + "translation": "सुरक्षित कनेक्शन सेवा सक्षम नहीं है।" + }, + { + "id": "api.command_remote.service_disabled", + "translation": "सुरक्षित कनेक्शन सेवा अक्षम है।" + }, + { + "id": "api.command_remote.remove_remote_id.help", + "translation": "हटाने के लिए सुरक्षित कनेक्शन की आईडी।" + }, + { + "id": "api.command_remote.remove_remote.error", + "translation": "सुरक्षित कनेक्शन नहीं निकाला जा सका: {{.Error}}" + }, + { + "id": "api.command_remote.remove.help", + "translation": "एक सुरक्षित कनेक्शन हटाता है" + }, + { + "id": "api.command_remote.remotes_not_found", + "translation": "कोई सुरक्षित कनेक्शन नहीं मिला।" + }, + { + "id": "api.command_remote.permission_required", + "translation": "सुरक्षित कनेक्शन प्रबंधित करने के लिए आपको `{{.Permission}}` अनुमति की आवश्यकता है।" + }, + { + "id": "api.command_remote.name.hint", + "translation": "सुरक्षित कनेक्शन के लिए एक अनूठा नाम" + }, + { + "id": "api.command_remote.name.help", + "translation": "सुरक्षित कनेक्शन का नाम" + }, + { + "id": "api.command_remote.name", + "translation": "सुरक्षित कनेक्शन" + }, + { + "id": "api.command_remote.missing_empty", + "translation": "गुम या खाली `{{.Arg}}`" + }, + { + "id": "api.command_remote.invite_password.hint", + "translation": "आमंत्रण को एन्क्रिप्ट करने के लिए उपयोग किया जाने वाला पासवर्ड" + }, + { + "id": "api.command_remote.invite_password.help", + "translation": "आमंत्रण पासवर्ड" + }, + { + "id": "api.command_remote.invite.help", + "translation": "सुरक्षित कनेक्शन आमंत्रित करें" + }, + { + "id": "api.command_remote.invitation_created", + "translation": "आमंत्रण बनाया गया." + }, + { + "id": "api.command_remote.invitation.hint", + "translation": "सुरक्षित कनेक्शन से एन्क्रिप्ट किया गया आमंत्रण" + }, + { + "id": "api.command_remote.invitation.help", + "translation": "सुरक्षित कनेक्शन से आमंत्रण" + }, + { + "id": "api.command_remote.incorrect_password.error", + "translation": "आमंत्रण को डिक्रिप्ट नहीं किया जा सका. गलत पासवर्ड या करप्ट आमंत्रण: {{.Error}}" + }, + { + "id": "api.command_remote.fetch_status.error", + "translation": "सुरक्षित कनेक्शन नहीं ला सके: {{.Error}}" + }, + { + "id": "api.command_remote.encrypt_invitation.error", + "translation": "आमंत्रण एन्क्रिप्ट नहीं किया जा सका: {{.Error}}" + }, + { + "id": "api.command_remote.displayname.hint", + "translation": "सुरक्षित कनेक्शन के लिए एक डिस्पले नाम" + }, + { + "id": "api.command_remote.displayname.help", + "translation": "सुरक्षित कनेक्शन डिस्पले नाम" + }, + { + "id": "api.command_remote.desc", + "translation": "मैटरमॉस्ट इंस्टेंस में संचार के लिए सुरक्षित कनेक्शन आमंत्रित करें।" + }, + { + "id": "api.command_remote.decode_invitation.error", + "translation": "आमंत्रण को डिकोड नहीं किया जा सका: {{.Error}}" + }, + { + "id": "api.command_remote.cluster_removed", + "translation": "सुरक्षित कनेक्शन {{.RemoteId}} {{.Result}}।" + }, + { + "id": "api.command_remote.add_remote.error", + "translation": "सुरक्षित कनेक्शन नहीं जोड़ा जा सका: {{.Error}}" + }, + { + "id": "api.command_remote.accept_invitation.error", + "translation": "आमंत्रण स्वीकार नहीं किया जा सका: {{.Error}}" + }, + { + "id": "api.command_remote.accept_invitation", + "translation": "आमंत्रण स्वीकार किया गया और पुष्टि की गई।\nसाइट URL: {{.SiteURL}}" + }, + { + "id": "api.command_remote.accept.help", + "translation": "किसी बाहरी मैटरमोस्ट इंस्टेंस से आमंत्रण स्वीकार करें" + }, + { + "id": "api.command_open.name", + "translation": "ओपन" + }, + { + "id": "api.command_online.success", + "translation": "अब आप ऑनलाइन हैं" + }, + { + "id": "api.command_online.name", + "translation": "ऑनलाइन" + }, + { + "id": "api.command_online.desc", + "translation": "अपना स्तेतस ऑनलाइन सेट करें" + }, + { + "id": "api.command_offline.success", + "translation": "अब आप ऑफ़लाइन हैं" + }, + { + "id": "api.command_offline.name", + "translation": "ऑफ़लाइन" + }, + { + "id": "api.command_offline.desc", + "translation": "अपना स्तेतस ऑफ़लाइन सेट करें" + }, + { + "id": "api.command_mute.success_unmute_direct_msg", + "translation": "यह चैनल अब म्यूट नहीं है।" + }, + { + "id": "api.command_mute.success_unmute", + "translation": "{{.Channel}} अब म्यूट नहीं है।" + }, + { + "id": "api.command_mute.success_mute_direct_msg", + "translation": "चैनल म्यूट बंद होने तक आपको इस चैनल के लिए नोटिफिकेशन प्राप्त नहीं होंगी।" + }, + { + "id": "api.command_mute.success_mute", + "translation": "चैनल म्यूट बंद होने तक आपको {{.Channel}} के लिए सूचनाएं प्राप्त नहीं होंगी।" + }, + { + "id": "api.command_mute.not_member.error", + "translation": "चैनल {{.Channel}} को म्यूट नहीं किया जा सका क्योंकि आप सदस्य नहीं हैं।" + }, + { + "id": "api.command_mute.no_channel.error", + "translation": "निर्दिष्ट चैनल नहीं मिल सका। चैनलों की पहचान करने के लिए कृपया [चैनल हैंडल](https://docs.mattermost.com/messaging/managing-channels.html#naming-a-channel) का उपयोग करें।" + }, + { + "id": "api.command_mute.name", + "translation": "म्यूट" + }, + { + "id": "api.command_mute.hint", + "translation": "~[चैनल]" + }, + { + "id": "api.command_mute.error", + "translation": "चैनल {{.Channel}} ढूंढा नहीं जा सका। चैनलों की पहचान करने के लिए कृपया [चैनल हैंडल](https://docs.mattermost.com/messaging/managing-channels.html#naming-a-channel) का उपयोग करें।" + }, + { + "id": "api.command_mute.desc", + "translation": "वर्तमान चैनल या निर्दिष्ट [चैनल] के लिए डेस्कटॉप, ईमेल और पुश नोटिफिकेशन बंद कर देता है।" + }, + { + "id": "model.preference.is_valid.value.app_error", + "translation": "वैल्यू बहुत लंबा है." + }, + { + "id": "app.channel.create_channel.no_team_id.app_error", + "translation": "चैनल बनाने के लिए टीम आईडी निर्दिष्ट करना आवश्यक है।" + }, + { + "id": "app.channel.create_channel.internal_error", + "translation": "चैनल सहेजने में असमर्थ." + }, + { + "id": "app.channel.count_posts_since.app_error", + "translation": "दी गई तिथि से मैसेजो की गणना करने में असमर्थ।" + }, + { + "id": "app.channel.clear_all_custom_role_assignments.select.app_error", + "translation": "चैनल के सदस्यों को पुनः प्राप्त करने में विफल।" + }, + { + "id": "app.channel.autofollow.app_error", + "translation": "उल्लिखित उपयोगकर्ता के लिए थ्रेड सदस्यता अपडेट करने में विफल" + }, + { + "id": "app.channel.analytics_type_count.app_error", + "translation": "चैनल प्रकार की गणना प्राप्त करने में असमर्थ।" + }, + { + "id": "app.bot.permenent_delete.bad_id", + "translation": "बॉट को मिटाने में असमर्थ." + }, + { + "id": "app.bot.permanent_delete.internal_error", + "translation": "बॉट को स्थायी रूप से हटाने में असमर्थ." + }, + { + "id": "app.bot.patchbot.internal_error", + "translation": "बॉट को अपडेट करने में असमर्थ।" + }, + { + "id": "app.bot.getbots.internal_error", + "translation": "बॉट प्राप्त करने में असमर्थ।" + }, + { + "id": "app.bot.getbot.internal_error", + "translation": "बॉट प्राप्त करने में असमर्थ।" + }, + { + "id": "app.bot.get_warn_metrics_bot.empty_admin_list.app_error", + "translation": "व्यवस्थापकों की लिस्ट खाली है।" + }, + { + "id": "app.bot.get_system_bot.empty_admin_list.app_error", + "translation": "व्यवस्थापकों की लिस्ट खाली है।" + }, + { + "id": "app.bot.createbot.internal_error", + "translation": "बॉट सहेजने में असमर्थ." + }, + { + "id": "app.audit.permanent_delete_by_user.app_error", + "translation": "हमें ऑडिट को हटाने में एक त्रुटि का सामना करना पड़ा।" + }, + { + "id": "app.audit.get.limit.app_error", + "translation": "पेजिंग के लिए सीमा पार हो गई है।" + }, + { + "id": "app.audit.get.finding.app_error", + "translation": "हमें ऑडिट खोजने में त्रुटि का सामना करना पड़ा।" + }, + { + "id": "app.analytics.getanalytics.internal_error", + "translation": "विश्लेषण प्राप्त करने में असमर्थ।" + }, + { + "id": "app.admin.test_site_url.failure", + "translation": "यह एक मान्य लाइव URL नहीं है" + }, + { + "id": "app.admin.test_email.failure", + "translation": "कनेक्शन असफल: {{.Error}}" + }, + { + "id": "app.admin.saml.failure_read_response_body_from_idp.app_error", + "translation": "पहचान प्रदाता से प्राप्त प्रतिक्रिया पेलोड को पढ़ते समय विफलता का सामना करना पड़ा।" + }, + { + "id": "app.admin.saml.failure_decode_metadata_xml_from_idp.app_error", + "translation": "पहचान प्रदाता से प्राप्त XML मेटाडेटा जानकारी को डीकोड नहीं कर सका।" + }, + { + "id": "api.websocket_handler.server_busy.app_error", + "translation": "सर्वर व्यस्त है, गैर-महत्वपूर्ण सेवाएं अस्थायी रूप से अनुपलब्ध हैं।" + }, + { + "id": "api.websocket_handler.invalid_param.app_error", + "translation": "अमान्य {{.Name}} पैरामीटर।" + }, + { + "id": "api.webhook.update_outgoing.intersect.app_error", + "translation": "एक ही चैनल के आउटगोइंग वेबहुक में समान ट्रिगर शब्द/कॉलबैक URL नहीं हो सकते।" + }, + { + "id": "api.webhook.team_mismatch.app_error", + "translation": "सभी टीमों में वेबहुक अपडेट करने में असमर्थ।" + }, + { + "id": "api.webhook.incoming.error", + "translation": "आने वाले वेबहुक के मल्टीपार्ट पेलोड को डिकोड नहीं कर सका।" + }, + { + "id": "api.webhook.create_outgoing.triggers.app_error", + "translation": "या तो trigger_words या channel_id सेट होना चाहिए।" + }, + { + "id": "api.webhook.create_outgoing.permissions.app_error", + "translation": "आउटगोइंग वेबहुक बनाने के लिए अमान्य अनुमतियां।" + }, + { + "id": "api.webhook.create_outgoing.not_open.app_error", + "translation": "आउटगोइंग वेबहुक केवल सार्वजनिक चैनलों के लिए बनाए जा सकते हैं।" + }, + { + "id": "api.webhook.create_outgoing.intersect.app_error", + "translation": "एक ही चैनल के आउटगोइंग वेबहुक में समान ट्रिगर शब्द/कॉलबैक URL नहीं हो सकते।" + }, + { + "id": "api.web_socket_router.not_authenticated.app_error", + "translation": "वेबसॉकेट कनेक्शन प्रमाणित नहीं है। कृपया पुनः लॉगिन करें और पुनः प्रयास करें।" + }, + { + "id": "api.web_socket_router.no_action.app_error", + "translation": "कोई वेबसोकेट कार्रवाई नहीं।" + }, + { + "id": "api.web_socket_router.bad_seq.app_error", + "translation": "WebSocket मैसेज के लिए अमान्य अनुक्रम।" + }, + { + "id": "api.web_socket_router.bad_action.app_error", + "translation": "अज्ञात वेबसॉकेट क्रिया।" + }, + { + "id": "api.web_socket.connect.upgrade.app_error", + "translation": "वेबसोकेट कनेक्शन को अपग्रेड करने में विफल।" + }, + { + "id": "api.user.verify_email.token_parse.error", + "translation": "ईमेल सत्यापन से टोकन डेटा पार्स करने में विफल" + }, + { + "id": "api.user.verify_email.link_expired.app_error", + "translation": "ईमेल सत्यापन लिंक की समय सीमा समाप्त हो गई है।" + }, + { + "id": "api.user.verify_email.broken_token.app_error", + "translation": "ईमेल टोकन प्रकार को खराब सत्यापित करें।" + }, + { + "id": "api.user.verify_email.bad_link.app_error", + "translation": "ईमेल लिंक खराब सत्यापित करें।" + }, + { + "id": "api.user.upload_profile_user.upload_profile.app_error", + "translation": "प्रोफ़ाइल इमेज अपलोड नहीं किया जा सका." + }, + { + "id": "api.user.upload_profile_user.too_large.app_error", + "translation": "प्रोफ़ाइल इमेज अपलोड करने में असमर्थ. फ़ाइल बहुत बड़ी है।" + }, + { + "id": "api.user.upload_profile_user.storage.app_error", + "translation": "फाइल अपलोड करने में असमर्थ। इमेज संग्रहण कॉन्फ़िगर नहीं किया गया है।" + }, + { + "id": "api.user.upload_profile_user.parse.app_error", + "translation": "मल्टीपार्ट फॉर्म को पार्स नहीं किया जा सका।" + }, + { + "id": "api.user.upload_profile_user.open.app_error", + "translation": "इमेज फ़ाइल नहीं खोल सका।" + }, + { + "id": "api.user.upload_profile_user.no_file.app_error", + "translation": "अनुरोध में 'इमेज' के अंतर्गत कोई फ़ाइल नहीं है।" + }, + { + "id": "api.user.upload_profile_user.login_provider_attribute_set.app_error", + "translation": "प्रोफ़ाइल इमेज उपयोगकर्ता के लॉगिन प्रदाता के माध्यम से सेट किया जाना चाहिए।" + }, + { + "id": "api.user.upload_profile_user.encode.app_error", + "translation": "प्रोफ़ाइल इमेज को एन्कोड नहीं किया जा सका." + }, + { + "id": "api.user.upload_profile_user.decode.app_error", + "translation": "प्रोफ़ाइल इमेज को डिकोड नहीं किया जा सका." + }, + { + "id": "api.user.upload_profile_user.check_image_limits.app_error", + "translation": "इमेज सीमा जांच विफल। संकल्प बहुत अधिक है।" + }, + { + "id": "api.user.update_user.login_provider_attribute_set.app_error", + "translation": "फ़ील्ड '{{.Field}}' को उपयोगकर्ता के लॉगिन प्रदाता के माध्यम से सेट किया जाना चाहिए।" + }, + { + "id": "api.user.update_user.accepted_guest_domain.app_error", + "translation": "आपके द्वारा प्रदान किया गया ईमेल अतिथि अकाऊंत के लिए स्वीकृत डोमेन से संबंधित नहीं है। कृपया अपने व्यवस्थापक से संपर्क करें या किसी दूसरे ईमेल से साइन अप करें।" + }, + { + "id": "api.user.update_user.accepted_domain.app_error", + "translation": "आपके द्वारा प्रदान किया गया ईमेल किसी स्वीकृत डोमेन से संबंधित नहीं है। कृपया अपने व्यवस्थापक से संपर्क करें या किसी दूसरे ईमेल से साइन अप करें।" + }, + { + "id": "api.user.update_password.valid_account.app_error", + "translation": "पासवर्ड अपडेट करना विफल रहा क्योंकि हमें कोई मान्य अकाऊंत नहीं मिला।" + }, + { + "id": "api.user.update_password.user_and_hashed.app_error", + "translation": "केवल सिस्टम व्यवस्थापक पहले से हैश किए गए पासवर्ड सेट कर सकते हैं।" + }, + { + "id": "api.user.update_password.oauth.app_error", + "translation": "पासवर्ड अपडेट करना विफल रहा क्योंकि उपयोगकर्ता एक OAuth सेवा के माध्यम से लॉग इन है।" + }, + { + "id": "api.user.update_password.menu", + "translation": "सेटिंग्स मेनू का उपयोग करना" + }, + { + "id": "api.user.update_password.incorrect.app_error", + "translation": "आपके द्वारा दर्ज किया गया \"वर्तमान पासवर्ड\" गलत है। कृपया जांचें कि कैप्स लॉक बंद है और पुनः प्रयास करें।" + }, + { + "id": "api.user.update_password.failed.app_error", + "translation": "पासवर्ड अपडेट करना विफल रहा।" + }, + { + "id": "api.user.update_password.context.app_error", + "translation": "पासवर्ड अपडेट करना विफल रहा क्योंकि संदर्भ user_id, प्रॉप्स user_id से मेल नहीं खाता।" + }, + { + "id": "api.user.update_oauth_user_attrs.get_user.app_error", + "translation": "{{.Service}} उपयोगकर्ता ऑब्जेक्ट से उपयोगकर्ता प्राप्त नहीं किया जा सका।" + }, + { + "id": "model.compliance.is_valid.id.app_error", + "translation": "अमान्य आइ डी।" + }, + { + "id": "model.compliance.is_valid.desc.app_error", + "translation": "अमान्य विवरण।" + }, + { + "id": "model.command_hook.user_id.app_error", + "translation": "अमान्य उपयोगकर्ता आइ डी।" + }, + { + "id": "model.command_hook.root_id.app_error", + "translation": "अमान्य रूट आईडी." + }, + { + "id": "model.command_hook.id.app_error", + "translation": "अमान्य कमांड हुक आईडी।" + }, + { + "id": "model.command_hook.command_id.app_error", + "translation": "अमान्य कमांड आईडी।" + }, + { + "id": "model.command_hook.channel_id.app_error", + "translation": "अमान्य चैनल आईडी." + }, + { + "id": "model.command.is_valid.user_id.app_error", + "translation": "अमान्य उपयोगकर्ता आइ डी।" + }, + { + "id": "model.command.is_valid.url_http.app_error", + "translation": "अमान्य URL। एक मान्य URL होना चाहिए और http:// या https:// से शुरू होना चाहिए।" + }, + { + "id": "model.command.is_valid.url.app_error", + "translation": "अमान्य URL।" + }, + { + "id": "model.command.is_valid.trigger.app_error", + "translation": "अमान्य ट्रिगर।" + }, + { + "id": "model.command.is_valid.token.app_error", + "translation": "अमान्य टोकन।" + }, + { + "id": "model.command.is_valid.team_id.app_error", + "translation": "अमान्य टीम आईडी।" + }, + { + "id": "model.command.is_valid.plugin_id.app_error", + "translation": "अमान्य प्लगइन आईडी।" + }, + { + "id": "model.command.is_valid.method.app_error", + "translation": "अमान्य विधि।" + }, + { + "id": "model.command.is_valid.id.app_error", + "translation": "अमान्य आइ डी।" + }, + { + "id": "model.command.is_valid.display_name.app_error", + "translation": "अमान्य शीर्षक." + }, + { + "id": "model.command.is_valid.description.app_error", + "translation": "अमान्य विवरण।" + }, + { + "id": "model.cluster.is_valid.id.app_error", + "translation": "अमान्य आइ डी।" + }, + { + "id": "model.cluster.is_valid.hostname.app_error", + "translation": "होस्टनाम सेट होना चाहिए।" + }, + { + "id": "model.channel_member.is_valid.user_id.app_error", + "translation": "अमान्य उपयोगकर्ता आइ डी।" + }, + { + "id": "model.channel_member.is_valid.roles_limit.app_error", + "translation": "अमान्य चैनल सदस्य भूमिकाएं {{.Limit}} वर्णों से अधिक लंबी हैं।" + }, + { + "id": "model.channel_member.is_valid.push_level.app_error", + "translation": "अमान्य पुश सूचना स्तर।" + }, + { + "id": "model.channel_member.is_valid.email_value.app_error", + "translation": "अमान्य ईमेल सूचना मान." + }, + { + "id": "model.channel_member.is_valid.channel_id.app_error", + "translation": "अमान्य चैनल आईडी." + }, + { + "id": "model.channel.is_valid.type.app_error", + "translation": "अमान्य प्रकार।" + }, + { + "id": "model.channel.is_valid.purpose.app_error", + "translation": "अमान्य उद्देश्य." + }, + { + "id": "model.channel.is_valid.id.app_error", + "translation": "अमान्य आइ डी।" + }, + { + "id": "model.channel.is_valid.header.app_error", + "translation": "अमान्य हैडर।" + }, + { + "id": "model.channel.is_valid.display_name.app_error", + "translation": "अमान्य प्रदर्शन नाम." + }, + { + "id": "model.channel.is_valid.creator_id.app_error", + "translation": "अमान्य क्रिएटर आईडी." + }, + { + "id": "model.bot.is_valid.username.app_error", + "translation": "अमान्य उपयोगकर्ता नाम।" + }, + { + "id": "model.bot.is_valid.user_id.app_error", + "translation": "अमान्य उपयोगकर्ता आइ डी।" + }, + { + "id": "model.bot.is_valid.description.app_error", + "translation": "अमान्य विवरण।" + }, + { + "id": "model.bot.is_valid.creator_id.app_error", + "translation": "अमान्य क्रिएटर आईडी." + }, + { + "id": "model.authorize.is_valid.user_id.app_error", + "translation": "अमान्य उपयोगकर्ता आइ डी।" + }, + { + "id": "model.authorize.is_valid.state.app_error", + "translation": "अमान्य स्थिति।" + }, + { + "id": "model.authorize.is_valid.scope.app_error", + "translation": "अमान्य दायरा।" + }, + { + "id": "model.authorize.is_valid.response_type.app_error", + "translation": "अमान्य प्रतिक्रिया प्रकार।" + }, + { + "id": "api.team.search_teams.pagination_not_implemented.public_team_search", + "translation": "केवल-सार्वजनिक टीम खोज के लिए पेजिनेशन लागू नहीं किया गया।" + }, + { + "id": "api.team.search_teams.pagination_not_implemented.private_team_search", + "translation": "केवल-निजी टीम खोज के लिए पेजिनेशन लागू नहीं किया गया।" + }, + { + "id": "api.team.remove_user_from_team.removed", + "translation": "%v को टीम से हटाया गया।" + }, + { + "id": "api.team.remove_user_from_team.missing.app_error", + "translation": "ऐसा लगता है कि उपयोगकर्ता इस टीम का हिस्सा नहीं है।" + }, + { + "id": "api.team.remove_team_icon.get_team.app_error", + "translation": "टीम प्राप्त करने में एक त्रुटि हुई।" + }, + { + "id": "api.team.remove_member.group_constrained.app_error", + "translation": "किसी उपयोगकर्ता को समूह-विवश टीम से निकालने में असमर्थ।" + }, + { + "id": "api.team.move_channel.success", + "translation": "इस चैनल को %v से इस टीम में स्थानांतरित कर दिया गया है।" + }, + { + "id": "api.team.leave.left", + "translation": "%v ने टीम छोड़ दी।" + }, + { + "id": "api.team.join_user_to_team.allowed_domains.app_error", + "translation": "उपयोगकर्ता को जोड़ा नहीं जा सकता क्योंकि अकाऊंत से जुड़े डोमेन की अनुमति नहीं है। अतिरिक्त विवरण के लिए अपने सिस्टम व्यवस्थापक से संपर्क करें।" + }, + { + "id": "api.team.join_team.post_and_forget", + "translation": "%v टीम में शामिल हुए।" + }, + { + "id": "api.team.is_team_creation_allowed.domain.app_error", + "translation": "उपयोगकर्ता को जोड़ा नहीं जा सकता क्योंकि अकाऊंत से जुड़े डोमेन की अनुमति नहीं है। अतिरिक्त विवरण के लिए अपने सिस्टम व्यवस्थापक से संपर्क करें।" + }, + { + "id": "api.team.is_team_creation_allowed.disabled.app_error", + "translation": "टीम निर्माण अक्षम कर दिया गया है। विवरण के लिए कृपया अपने सिस्टम व्यवस्थापक से पूछें।" + }, + { + "id": "api.team.invite_members.no_one.app_error", + "translation": "आमंत्रित करने वाला कोई नहीं।" + }, + { + "id": "api.team.invite_members.invalid_email.app_error", + "translation": "निम्नलिखित ईमेल एक स्वीकृत डोमेन से संबंधित नहीं हैं: {{.Addresses}}। विवरण के लिए कृपया अपने सिस्टम व्यवस्थापक से संपर्क करें।" + }, + { + "id": "api.team.invite_members.disabled.app_error", + "translation": "ईमेल आमंत्रण अक्षम हैं।" + }, + { + "id": "api.team.invite_guests.channel_in_invalid_team.app_error", + "translation": "आमंत्रण के चैनल आमंत्रण की टीम का हिस्सा होना चाहिए।" + }, + { + "id": "api.team.invalidate_all_email_invites.app_error", + "translation": "ईमेल आमंत्रणों को अमान्य करने में त्रुटि." + }, + { + "id": "api.team.import_team.parse.app_error", + "translation": "मल्टीपार्ट फॉर्म को पार्स नहीं किया जा सका।" + }, + { + "id": "api.team.import_team.open.app_error", + "translation": "फ़ाइल को खोल नहीं सका।" + }, + { + "id": "api.team.import_team.no_file.app_error", + "translation": "अनुरोध में 'फ़ाइल' के अंतर्गत कोई फ़ाइल नहीं है।" + }, + { + "id": "api.team.get_team_icon.read_file.app_error", + "translation": "टीम आइकन फ़ाइल को पढ़ने में असमर्थ।" + }, + { + "id": "api.team.get_team_icon.filesettings_no_driver.app_error", + "translation": "फ़ाइल सेटिंग्स के लिए अमान्य ड्राइवर नाम। 'local' या 'amazons3' होना चाहिए।" + }, + { + "id": "api.team.get_invite_info.not_open_team", + "translation": "आमंत्रण अमान्य है क्योंकि यह एक खुली टीम नहीं है।" + }, + { + "id": "api.team.get_all_teams.insufficient_permissions", + "translation": "आपके पास सभी टीमों को लिस्ट करने के लिए उपयुक्त अनुमति नहीं है" + }, + { + "id": "api.team.demote_user_to_guest.license.error", + "translation": "आपका लाइसेंस अतिथि अकाऊंत का समर्थन नहीं करता" + }, + { + "id": "api.team.demote_user_to_guest.disabled.error", + "translation": "अतिथि अकाऊंत अक्षम हैं।" + }, + { + "id": "api.team.add_user_to_team_from_invite.guest.app_error", + "translation": "मेहमानों को एक आमंत्रण लिंक के माध्यम से टीम में शामिल होने से प्रतिबंधित किया गया है। कृपया टीम को अतिथि ईमेल आमंत्रण का अनुरोध करें।" + }, + { + "id": "api.team.add_user_to_team.missing_parameter.app_error", + "translation": "उपयोगकर्ता को टीम में जोड़ने के लिए आवश्यक पैरामीटर।" + }, + { + "id": "api.team.add_user_to_team.added", + "translation": "%v को %v द्वारा टीम में जोड़ा गया।" + }, + { + "id": "api.team.add_members.user_denied", + "translation": "इस टीम का प्रबंधन समूहों द्वारा किया जाता है। यह उपयोगकर्ता उस समूह का हिस्सा नहीं है जो इस टीम से समन्वयित है।" + }, + { + "id": "api.team.add_members.error", + "translation": "टीम सदस्य(सदस्यों) को जोड़ने में त्रुटि।" + }, + { + "id": "api.system.update_viewed_notices.failed", + "translation": "देखे गए नोटिस को अपडेट करना विफल रहा" + }, + { + "id": "api.system.update_notices.parse_failed", + "translation": "उत्पाद नोटिस पार्स करना विफल रहा" + }, + { + "id": "api.system.update_notices.fetch_failed", + "translation": "उत्पाद नोटिस फ़ेच करना विफल रहा" + }, + { + "id": "api.system.update_notices.clear_failed", + "translation": "पुराने उत्पाद नोटिस को साफ़ करना विफल रहा" + }, + { + "id": "api.system.id_loaded.not_available.app_error", + "translation": "आईडी लोडेड पुश नोटिफिकेशन इस सर्वर पर कॉन्फ़िगर या समर्थित नहीं हैं।" + }, + { + "id": "api.status.user_not_found.app_error", + "translation": "उपयोगकर्ता नहीं मिला।" + }, + { + "id": "api.slackimport.slack_import.zip.file_too_large", + "translation": "{{.Filename}} ज़िप संग्रह में स्लैक आयात के लिए संसाधित करने के लिए बहुत बड़ा है\n" + }, + { + "id": "api.slackimport.slack_import.zip.app_error", + "translation": "स्लैक एक्सपोर्ट ज़िप फ़ाइल को खोलने में असमर्थ।\n" + }, + { + "id": "api.slackimport.slack_import.team_fail", + "translation": "टीम को आयात करने में असमर्थ।\n" + }, + { + "id": "api.slackimport.slack_import.open.app_error", + "translation": "फ़ाइल खोलने में असमर्थ: {{.Filename}}.\n" + }, + { + "id": "api.slackimport.slack_import.notes", + "translation": "\nनोट्स:\n" + }, + { + "id": "api.slackimport.slack_import.note3", + "translation": "- सर्वर लॉग में अतिरिक्त त्रुटियां मिल सकती हैं।\n" + }, + { + "id": "api.slackimport.slack_import.note2", + "translation": "- स्लैक बॉट मैसेज वर्तमान में समर्थित नहीं हैं\n" + }, + { + "id": "api.slackimport.slack_import.note1", + "translation": "- कुछ मैसेज आयात नहीं किए गए होंगे क्योंकि वे इस आयातक द्वारा समर्थित नहीं थे।\n" + }, + { + "id": "api.slackimport.slack_import.log", + "translation": "मैटरमोस्ट स्लैक आयात लॉग\n" + }, + { + "id": "api.slackimport.slack_add_users.unable_import", + "translation": "स्लैक उपयोगकर्ता आयात करने में असमर्थ: {{.Username}}।\n" + }, + { + "id": "api.slackimport.slack_add_users.missing_email_address", + "translation": "उपयोगकर्ता {{.Username}} का स्लैक एक्सपोर्ट में कोई ईमेल पता नहीं है। {{.Email}} को प्लेसहोल्डर के रूप में इस्तेमाल किया। एक बार सिस्टम में लॉग इन करने के बाद उपयोगकर्ता को अपना ईमेल अपडेट करना चाहिए।\n" + }, + { + "id": "api.slackimport.slack_add_users.email_pwd", + "translation": "ईमेल {{.Email}} और पासवर्ड {{.Password}} के साथ स्लैक उपयोगकर्ता को आयात किया गया है।\n" + }, + { + "id": "api.slackimport.slack_add_users.created", + "translation": "\nउपयोगकर्ता बनाए गए:\n" + }, + { + "id": "api.slackimport.slack_add_channels.merge", + "translation": "स्लैक चैनल {{.DisplayName}} पहले से एक सक्रिय मैटरमॉस्ट चैनल के रूप में मौजूद है। दोनों चैनलों को मिला दिया गया है।\n" + }, + { + "id": "api.slackimport.slack_add_channels.import_failed", + "translation": "स्लैक चैनल {{.DisplayName}} आयात करने में असमर्थ।\n" + }, + { + "id": "api.slackimport.slack_add_channels.failed_to_add_user", + "translation": "चैनल में स्लैक यूजर {{.Username}} जोड़ने में असमर्थ।\n" + }, + { + "id": "api.slackimport.slack_add_channels.added", + "translation": "\nचैनल जोड़े गए:\n" + }, + { + "id": "api.slackimport.slack_add_bot_user.unable_import", + "translation": "इंटीग्रेशन/स्लैक बॉट उपयोगकर्ता {{.Username}} आयात करने में असमर्थ\n" + }, + { + "id": "api.slackimport.slack_add_bot_user.email_pwd", + "translation": "ईमेल {{.Email}} और पासवर्ड {{.Password}} के साथ इंटीग्रेशन/स्लैक बॉट उपयोगकर्ता आयात किया गया है।\n" + }, + { + "id": "api.server.warn_metric.number_of_teams_5.start_trial_notification_success.message", + "translation": "आपका एंटरप्राइज़ परीक्षण अब सक्रिय है। उन्नत अनुमतियां सक्षम करने के लिए **सिस्टम कंसोल> उपयोगकर्ता प्रबंधन> अनुमतियां** पर जाएं।" + }, + { + "id": "api.channel.create_channel.direct_channel.team_restricted_error", + "translation": "इन उपयोगकर्ताओं के बीच एक सीधा चैनल नहीं बनाया जा सकता क्योंकि वे एक टीम को साझा नहीं करते हैं।" + }, + { + "id": "api.channel.create_channel.direct_channel.app_error", + "translation": "सीधे संदेश चैनल निर्माण के लिए createDirectChannel API सेवा का उपयोग करना चाहिए।" + }, + { + "id": "api.admin.upload_brand_image.storage.app_error", + "translation": "छवि अपलोड करने में असमर्थ. छवि संग्रहण कॉन्फ़िगर नहीं किया गया है।" + }, + { + "id": "api.admin.upload_brand_image.no_file.app_error", + "translation": "अनुरोध में 'छवि' के अंतर्गत कोई फ़ाइल नहीं है।" + }, + { + "id": "api.admin.upload_brand_image.array.app_error", + "translation": "अनुरोध में 'छवि' के तहत खाली सरणी।" + }, + { + "id": "api.admin.saml.set_certificate_from_metadata.missing_content_type.app_error", + "translation": "सामग्री का प्रकार मौजूद नहीं है." + }, + { + "id": "app.webhooks.save_incoming.existing.app_error", + "translation": "आप किसी मौजूदा इनकमिंग वेबहुक को अधिलेखित नहीं कर सकते हैं।" + }, + { + "id": "app.webhooks.save_incoming.app_error", + "translation": "इनकमिंगवेबहुक को सहेजने में असमर्थ।" + }, + { + "id": "app.webhooks.permanent_delete_outgoing_by_user.app_error", + "translation": "वेबहुक हटाने में असमर्थ." + }, + { + "id": "app.webhooks.permanent_delete_outgoing_by_channel.app_error", + "translation": "वेबहुक हटाने में असमर्थ." + }, + { + "id": "app.webhooks.permanent_delete_incoming_by_user.app_error", + "translation": "वेबहुक हटाने में असमर्थ." + }, + { + "id": "app.webhooks.permanent_delete_incoming_by_channel.app_error", + "translation": "वेबहुक हटाने में असमर्थ." + }, + { + "id": "app.webhooks.get_outgoing_by_team.app_error", + "translation": "वेबहुक प्राप्त करने में असमर्थ।" + }, + { + "id": "app.webhooks.get_outgoing_by_channel.app_error", + "translation": "वेबहुक प्राप्त करने में असमर्थ।" + }, + { + "id": "app.webhooks.get_outgoing.app_error", + "translation": "वेबहुक प्राप्त करने में असमर्थ।" + }, + { + "id": "app.webhooks.get_incoming_by_user.app_error", + "translation": "वेबहुक प्राप्त करने में असमर्थ।" + }, + { + "id": "app.webhooks.get_incoming_by_channel.app_error", + "translation": "वेबहुक प्राप्त करने में असमर्थ।" + }, + { + "id": "app.webhooks.get_incoming.app_error", + "translation": "वेबहुक प्राप्त करने में असमर्थ।" + }, + { + "id": "app.webhooks.delete_outgoing.app_error", + "translation": "वेबहुक हटाने में असमर्थ." + }, + { + "id": "app.webhooks.delete_incoming.app_error", + "translation": "वेबहुक हटाने में असमर्थ." + }, + { + "id": "app.webhooks.analytics_outgoing_count.app_error", + "translation": "आउटगोइंग वेबहुक की गणना करने में असमर्थ।" + }, + { + "id": "app.webhooks.analytics_incoming_count.app_error", + "translation": "आने वाले वेबहुक की गणना करने में असमर्थ।" + }, + { + "id": "app.valid_password_generic.app_error", + "translation": "पासवर्ड मान्य नहीं है" + }, + { + "id": "bleveengine.already_started.error", + "translation": "Bleve पहले ही शुरू हो चुका है।" + }, + { + "id": "api.channel.delete_channel.cannot.app_error", + "translation": "डिफ़ॉल्ट चैनल {{.Channel}} को हटाने में असमर्थ।" + }, + { + "id": "api.channel.delete_channel.archived", + "translation": "%v ने चैनल को आर्काइव किया।" + }, + { + "id": "api.channel.create_group.bad_user.app_error", + "translation": "प्रदान किए गए उपयोगकर्ताओं में से एक मौजूद नहीं है।" + }, + { + "id": "api.channel.create_group.bad_size.app_error", + "translation": "समूह संदेश चैनलों में कम से कम 3 और 8 से अधिक उपयोगकर्ता नहीं होने चाहिए।" + }, + { + "id": "api.channel.create_channel.max_channel_limit.app_error", + "translation": "वर्तमान टीम के लिए {{.MaxChannelsPerTeam}} से अधिक चैनल बनाने में असमर्थ।" + }, + { + "id": "app.webhooks.update_outgoing.app_error", + "translation": "वेबहुक को अपडेट करने में असमर्थ।" + }, + { + "id": "model.authorize.is_valid.client_id.app_error", + "translation": "अमान्य क्लाइंट आईडी." + }, + { + "id": "model.authorize.is_valid.auth_code.app_error", + "translation": "अमान्य प्राधिकरण कोड।" + }, + { + "id": "model.access.is_valid.user_id.app_error", + "translation": "अमान्य उपयोगकर्ता आइ डी।" + }, + { + "id": "model.access.is_valid.refresh_token.app_error", + "translation": "अमान्य रीफ़्रेश टोकन." + }, + { + "id": "model.access.is_valid.redirect_uri.app_error", + "translation": "अमान्य रीडायरेक्ट uri." + }, + { + "id": "app.import.import_channel.team_not_found.error", + "translation": "चैनल आयात करने में त्रुटि. \"{{.TeamName}}\" नाम वाली टीम नहीं मिली।" + }, + { + "id": "app.import.get_users_by_username.some_users_not_found.error", + "translation": "कुछ उपयोगकर्ता नहीं मिले" + }, + { + "id": "app.import.get_teams_by_names.some_teams_not_found.error", + "translation": "कुछ टीमें नहीं मिलीं" + }, + { + "id": "app.import.generate_password.app_error", + "translation": "पासवर्ड जनरेट करने में त्रुटि।" + }, + { + "id": "app.import.bulk_import.json_decode.error", + "translation": "लाइन का JSON डिकोड विफल रहा।" + }, + { + "id": "app.import.attachment.file_upload.error", + "translation": "फ़ाइल अपलोड करने में त्रुटि: \"{{.FilePath}}\"" + }, + { + "id": "app.import.attachment.bad_file.error", + "translation": "फ़ाइल को यहां पढ़ने में त्रुटि: \"{{.FilePath}}\"" + }, + { + "id": "app.file_info.save.app_error", + "translation": "फ़ाइल जानकारी सहेजने में असमर्थ." + }, + { + "id": "app.file_info.get_with_options.app_error", + "translation": "विकल्पों के साथ फ़ाइल जानकारी प्राप्त करने में असमर्थ" + }, + { + "id": "app.file_info.get_for_post.app_error", + "translation": "पोस्ट के लिए फ़ाइल जानकारी प्राप्त करने में असमर्थ." + }, + { + "id": "app.file_info.get.app_error", + "translation": "फ़ाइल जानकारी प्राप्त करने में असमर्थ।" + }, + { + "id": "app.export.export_custom_emoji.copy_emoji_images.error", + "translation": "कस्टम इमोजी इमेज कॉपी करने में असमर्थ" + }, + { + "id": "app.emoji.get_list.internal_error", + "translation": "इमोजी प्राप्त करने में असमर्थ।" + }, + { + "id": "app.emoji.get_by_name.no_result", + "translation": "हमें इमोजी नहीं मिला." + }, + { + "id": "app.emoji.get_by_name.app_error", + "translation": "इमोजी प्राप्त करने में असमर्थ।" + }, + { + "id": "app.emoji.get.no_result", + "translation": "हमें इमोजी नहीं मिला." + }, + { + "id": "app.emoji.get.app_error", + "translation": "इमोजी प्राप्त करने में असमर्थ।" + }, + { + "id": "app.emoji.delete.no_results", + "translation": "हमें मिटाने के लिए इमोजी नहीं मिला." + }, + { + "id": "app.emoji.delete.app_error", + "translation": "इमोजी मिटाने में असमर्थ." + }, + { + "id": "app.emoji.create.internal_error", + "translation": "इमोजी सहेजने में असमर्थ." + }, + { + "id": "app.email.rate_limit_exceeded.app_error", + "translation": "आमंत्रण ईमेल की दर सीमा पार हो गई है। टाइमर {{.ResetAfter}} सेकंड के बाद रीसेट हो जाएगा। कृपया {{.RetryAfter}} सेकंड के बाद पुन: प्रयास करें।" + }, + { + "id": "app.create_basic_user.save_member.max_accounts.app_error", + "translation": "डिफ़ॉल्ट टीम सदस्यता बनाने में असमर्थ क्योंकि उस टीम में और सदस्यों की अनुमति नहीं है" + }, + { + "id": "app.create_basic_user.save_member.conflict.app_error", + "translation": "डिफ़ॉल्ट टीम सदस्यता बनाने में असमर्थ क्योंकि वे पहले से मौजूद हैं" + }, + { + "id": "app.create_basic_user.save_member.app_error", + "translation": "डिफ़ॉल्ट टीम सदस्यता बनाने में असमर्थ" + }, + { + "id": "app.compliance.save.saving.app_error", + "translation": "अनुपालन रिपोर्ट को सहेजने में हमें एक त्रुटि का सामना करना पड़ा।" + }, + { + "id": "app.compliance.get.finding.app_error", + "translation": "हमें अनुपालन रिपोर्ट प्राप्त करने में त्रुटि का सामना करना पड़ा।" + }, + { + "id": "app.command_webhook.try_use.invalid", + "translation": "अमान्य वेबहुक." + }, + { + "id": "app.command_webhook.try_use.internal_error", + "translation": "वेबहुक का उपयोग करने में असमर्थ।" + }, + { + "id": "app.command_webhook.handle_command_webhook.parse", + "translation": "आने वाले डेटा को पार्स करने में असमर्थ।" + }, + { + "id": "app.command_webhook.get.missing", + "translation": "वेबहुक ढूँढने में असमर्थ." + }, + { + "id": "app.command_webhook.get.internal_error", + "translation": "वेबहुक प्राप्त करने में असमर्थ।" + }, + { + "id": "app.command_webhook.create_command_webhook.internal_error", + "translation": "कमांडवेबहुक को सहेजने में असमर्थ।" + }, + { + "id": "app.command_webhook.create_command_webhook.existing", + "translation": "आप मौजूदा कमांडवेबहुक को अपडेट नहीं कर सकते।" + }, + { + "id": "api.user.update_active.permissions.app_error", + "translation": "आपके पास उचित अनुमतियां नहीं हैं।" + }, + { + "id": "api.user.update_active.not_enable.app_error", + "translation": "आप स्वयं को निष्क्रिय नहीं कर सकते क्योंकि यह सुविधा सक्षम नहीं है। कृपया अपने सिस्टम व्यवस्थापक से संपर्क करें।" + }, + { + "id": "api.user.send_password_reset.sso.app_error", + "translation": "SSO अकाऊंत के लिए पासवर्ड रीसेट करने में असमर्थ।" + }, + { + "id": "api.user.send_password_reset.send.app_error", + "translation": "पासवर्ड रीसेट ईमेल सफलतापूर्वक भेजने में विफल।" + }, + { + "id": "api.user.send_cloud_welcome_email.error", + "translation": "क्लाउड स्वागत ईमेल भेजने में विफल" + }, + { + "id": "api.user.saml.not_available.app_error", + "translation": "SAML 2.0 इस सर्वर पर कॉन्फ़िगर या समर्थित नहीं है।" + }, + { + "id": "api.user.reset_password.token_parse.error", + "translation": "रीसेट पासवर्ड टोकन को पार्स करने में असमर्थ" + }, + { + "id": "api.user.reset_password.sso.app_error", + "translation": "SSO अकाऊंत के लिए पासवर्ड रीसेट करने में असमर्थ।" + }, + { + "id": "api.user.reset_password.method", + "translation": "एक रीसेट पासवर्ड लिंक का उपयोग करना" + }, + { + "id": "api.user.reset_password.link_expired.app_error", + "translation": "पासवर्ड रीसेट लिंक की समय सीमा समाप्त हो गई है।" + }, + { + "id": "api.user.reset_password.invalid_link.app_error", + "translation": "रीसेट पासवर्ड लिंक मान्य प्रतीत नहीं होता है।" + }, + { + "id": "api.user.reset_password.broken_token.app_error", + "translation": "रीसेट पासवर्ड टोकन मान्य प्रतीत नहीं होता है।" + }, + { + "id": "api.user.promote_guest_to_user.no_guest.app_error", + "translation": "अतिथि को नियमित उपयोगकर्ता में बदलने में असमर्थ क्योंकि वह अतिथि नहीं है।" + }, + { + "id": "api.user.patch_user.login_provider_attribute_set.app_error", + "translation": "फ़ील्ड '{{.Field}}' को उपयोगकर्ता के लॉगिन प्रदाता के माध्यम से सेट किया जाना चाहिए।" + }, + { + "id": "api.user.oauth_to_email.context.app_error", + "translation": "पासवर्ड अपडेट करना विफल रहा क्योंकि संदर्भ user_id प्रदान किए गए उपयोगकर्ता की आईडी से मेल नहीं खाता।" + }, + { + "id": "api.user.login_ldap.not_available.app_error", + "translation": "इस सर्वर पर AD/LDAP उपलब्ध नहीं है।" + }, + { + "id": "api.user.login_cws.license.error", + "translation": "CWS लॉगिन निषिद्ध है।" + }, + { + "id": "api.user.login_by_oauth.parse.app_error", + "translation": "{{.Service}} उपयोगकर्ता ऑब्जेक्ट से प्रमाणीकरण डेटा पार्स नहीं किया जा सका।" + }, + { + "id": "api.user.login_by_oauth.not_available.app_error", + "translation": "{{.Service}} OAuth 2.0 के माध्यम से SSO इस सर्वर पर उपलब्ध नहीं है।" + }, + { + "id": "api.user.login_by_oauth.bot_login_forbidden.app_error", + "translation": "बॉट लॉगिन निषिद्ध है।" + }, + { + "id": "api.user.login_by_cws.invalid_token.app_error", + "translation": "CWS टोकन मान्य नहीं है" + }, + { + "id": "api.channel.leave.default.app_error", + "translation": "डिफ़ॉल्ट चैनल {{.Channel}} को छोड़ने में असमर्थ।" + }, + { + "id": "api.channel.join_channel.post_and_forget", + "translation": "%v चैनल में शामिल हुए।" + }, + { + "id": "api.channel.join_channel.permissions.app_error", + "translation": "आपके पास उचित अनुमतियां नहीं हैं।" + }, + { + "id": "api.channel.guest_join_channel.post_and_forget", + "translation": "%v अतिथि के रूप में चैनल में शामिल हुए।" + }, + { + "id": "api.channel.get_channel_moderations.license.error", + "translation": "आपका लाइसेंस चैनल मॉडरेशन का समर्थन नहीं करता" + }, + { + "id": "api.channel.delete_channel.deleted.app_error", + "translation": "चैनल को संग्रहीत या हटा दिया गया है।" + }, + { + "id": "store.sql_channel.save_channel.existing.app_error", + "translation": "मौजूदा चैनल के लिए कॉल अपडेट अवश्य करें।" + }, + { + "id": "store.sql_channel.save.archived_channel.app_error", + "translation": "आप किसी संग्रहीत चैनल को संशोधित नहीं कर सकते हैं।" + }, + { + "id": "sharedchannel.cannot_deliver_post", + "translation": "एक या अधिक पोस्ट दूरस्थ साइट {{.Remote}} पर डिलीवर नहीं की जा सकी क्योंकि यह ऑफ़लाइन है। साइट के ऑनलाइन होने पर पोस्ट डिलीवर कर दी जाएगी।" + }, + { + "id": "plugin.api.get_users_in_channel", + "translation": "उपयोगकर्ता प्राप्त करने में असमर्थ, अमान्य छँटाई मानदंड।" + }, + { + "id": "model.user_access_token.is_valid.user_id.app_error", + "translation": "अमान्य उपयोगकर्ता आइ डी।" + }, + { + "id": "model.user.is_valid.password_limit.app_error", + "translation": "bcrypt की सीमाओं के कारण 72 वर्णों से अधिक पासवर्ड सेट करने में असमर्थ।" + }, + { + "id": "model.user.is_valid.last_name.app_error", + "translation": "अमान्य उपनाम." + }, + { + "id": "model.user.is_valid.id.app_error", + "translation": "अमान्य उपयोगकर्ता आईडी।" + }, + { + "id": "model.user.is_valid.first_name.app_error", + "translation": "अमान्य प्रथम नाम." + }, + { + "id": "model.user.is_valid.email.app_error", + "translation": "अमान्य ईमेल।" + }, + { + "id": "model.team.is_valid.id.app_error", + "translation": "अमान्य आईडी." + }, + { + "id": "model.team.is_valid.description.app_error", + "translation": "अमान्य विवरण।" + }, + { + "id": "model.session.is_valid.roles_limit.app_error", + "translation": "अमान्य सत्र भूमिकाएँ {{.Limit}} वर्णों से अधिक लंबी हैं।" + }, + { + "id": "model.post.is_valid.root_id.app_error", + "translation": "अमान्य रूट आईडी." + }, + { + "id": "model.post.is_valid.props.app_error", + "translation": "अमान्य प्रॉप्स।" + }, + { + "id": "model.post.is_valid.id.app_error", + "translation": "अमान्य आईडी।" + }, + { + "id": "model.post.is_valid.original_id.app_error", + "translation": "अमान्य मूल आईडी." + }, + { + "id": "model.plugin_kvset_options.is_valid.old_value.app_error", + "translation": "अमान्य पुराना मान, इसे तब सेट नहीं किया जाना चाहिए जब ऑपरेशन परमाणु न हो।" + }, + { + "id": "model.outgoing_hook.is_valid.team_id.app_error", + "translation": "अमान्य टीम आईडी।" + }, + { + "id": "model.outgoing_hook.is_valid.id.app_error", + "translation": "अमान्य आईडी." + }, + { + "id": "model.outgoing_hook.is_valid.display_name.app_error", + "translation": "अमान्य शीर्षक." + }, + { + "id": "model.outgoing_hook.is_valid.description.app_error", + "translation": "अमान्य विवरण।" + }, + { + "id": "model.outgoing_hook.is_valid.content_type.app_error", + "translation": "content_type के लिए अमान्य मान." + }, + { + "id": "model.outgoing_hook.is_valid.channel_id.app_error", + "translation": "अमान्य चैनल आईडी." + }, + { + "id": "model.outgoing_hook.is_valid.callback.app_error", + "translation": "अमान्य कॉलबैक URL।" + }, + { + "id": "model.outgoing_hook.icon_url.app_error", + "translation": "अमान्य आइकन।" + }, + { + "id": "model.oauth.is_valid.name.app_error", + "translation": "अमान्य नाम।" + }, + { + "id": "model.oauth.is_valid.icon_url.app_error", + "translation": "आइकॉन URL एक मान्य URL होना चाहिए और http:// या https:// से शुरू होना चाहिए." + }, + { + "id": "model.oauth.is_valid.homepage.app_error", + "translation": "होमपेज एक मान्य URL होना चाहिए और http:// या https:// से शुरू होना चाहिए।" + }, + { + "id": "system.message.name", + "translation": "सिस्टम" + }, + { + "id": "api.plugin.install.download_failed.app_error", + "translation": "प्लगइन डाउनलोड करते समय एक त्रुटि हुई।" + }, + { + "id": "api.outgoing_webhook.disabled.app_error", + "translation": "सिस्टम व्यवस्थापक द्वारा आउटगोइंग वेबहुक अक्षम कर दिए गए हैं।" + }, + { + "id": "api.oauth.singup_with_oauth.invalid_link.app_error", + "translation": "साइनअप लिंक मान्य प्रतीत नहीं होता है।" + }, + { + "id": "api.oauth.singup_with_oauth.expired_link.app_error", + "translation": "साइनअप लिंक की समय सीमा समाप्त हो गई है।" + }, + { + "id": "api.oauth.singup_with_oauth.disabled.app_error", + "translation": "उपयोगकर्ता साइन-अप अक्षम है।" + }, + { + "id": "api.oauth.register_oauth_app.turn_off.app_error", + "translation": "सिस्टम व्यवस्थापक ने OAuth2 सेवा प्रदाता को बंद कर दिया है।" + }, + { + "id": "api.oauth.invalid_state_token.app_error", + "translation": "अमान्य स्टेट टोकन।" + }, + { + "id": "api.oauth.authorize_oauth.disabled.app_error", + "translation": "सिस्टम व्यवस्थापक ने OAuth2 सेवा प्रदाता को बंद कर दिया है।" + }, + { + "id": "api.oauth.allow_oauth.turn_off.app_error", + "translation": "सिस्टम व्यवस्थापक ने OAuth2 सेवा प्रदाता को बंद कर दिया है।" + }, + { + "id": "api.migrate_to_saml.error", + "translation": "SAML माइग्रेट करने में असमर्थ." + }, + { + "id": "api.marshal_error", + "translation": "मार्शल करने में विफल।" + }, + { + "id": "api.license.upgrade_needed.app_error", + "translation": "फ़ीचर को एंटरप्राइज़ संस्करण में अपग्रेड करने की आवश्यकता है।" + }, + { + "id": "api.license.request_trial_license.fail_get_user_count.app_error", + "translation": "परीक्षण लाइसेंस प्राप्त करने में असमर्थ, कृपया पुनः प्रयास करें या support@mattermost.com से संपर्क करें। पंजीकृत उपयोगकर्ताओं की संख्या प्राप्त नहीं कर सकता।" + }, + { + "id": "api.license.request_trial_license.app_error", + "translation": "परीक्षण लाइसेंस प्राप्त करने में असमर्थ, कृपया पुनः प्रयास करें या support@mattermost.com से संपर्क करें।" + }, + { + "id": "api.license.request-trial.can-start-trial.not-allowed", + "translation": "मैटरमॉस्ट एंटरप्राइज संस्करण के लिए यह परीक्षण लाइसेंस कुंजी समाप्त हो गई है और अब मान्य नहीं है। यदि आप अपनी परीक्षण अवधि बढ़ाना चाहते हैं तो कृपया [हमारी सेल्स टीम से संपर्क करें](https://mattermost.com/contact-us/)।" + }, + { + "id": "api.license.request-trial.can-start-trial.error", + "translation": "जाँच नहीं की जा सकी कि क्या परीक्षण शुरू किया जा सकता है" + }, + { + "id": "api.license.request-trial.bad-request.terms-not-accepted", + "translation": "लाइसेंस का अनुरोध करने के लिए आपको मैटरमोस्ट सॉफ़्टवेयर मूल्यांकन अनुबंध और गोपनीयता नीति को स्वीकार करना होगा।" + }, + { + "id": "api.license.request-trial.bad-request", + "translation": "अनुरोध किए गए उपयोगकर्ताओं की संख्या सही नहीं है।" + }, + { + "id": "api.license.remove_expired_license.failed.error", + "translation": "अक्षम लाइसेंस ईमेल सफलतापूर्वक भेजने में विफल।" + }, + { + "id": "api.license.add_license.unique_users.app_error", + "translation": "यह लाइसेंस केवल {{.Users}} उपयोगकर्ताओं का समर्थन करता है, जब आपके सिस्टम में {{.Count}} अद्वितीय उपयोगकर्ता हों। अद्वितीय उपयोगकर्ताओं की गणना ईमेल द्वारा स्पष्ट रूप से की जाती है। आप साइट रिपोर्ट्स -> सांख्यिकी देखें के अंतर्गत कुल उपयोगकर्ता संख्या देख सकते हैं।" + }, + { + "id": "api.license.add_license.save_active.app_error", + "translation": "सक्रिय लाइसेंस आईडी ठीक से सहेजा नहीं गया।" + }, + { + "id": "api.license.add_license.save.app_error", + "translation": "लाइसेंस ठीक से सहेजा नहीं गया।" + }, + { + "id": "api.license.add_license.open.app_error", + "translation": "लाइसेंस फ़ाइल नहीं खोल सका." + }, + { + "id": "api.license.add_license.invalid.app_error", + "translation": "अमान्य लाइसेंस फ़ाइल।" + }, + { + "id": "api.license.add_license.expired.app_error", + "translation": "लाइसेंस या तो समाप्त हो गया है या अभी तक शुरू नहीं हुआ है।" + }, + { + "id": "api.ldap_groups.license_error", + "translation": "आपका लाइसेंस ldap समूहों का समर्थन नहीं करता" + }, + { + "id": "api.ldap_groups.existing_user_name_error", + "translation": "समूह नाम पहले से उपयोगकर्ता नाम के रूप में मौजूद है" + }, + { + "id": "api.ldap_groups.existing_reserved_name_error", + "translation": "समूह का नाम पहले से ही आरक्षित नाम के रूप में मौजूद है" + }, + { + "id": "api.ldap_groups.existing_group_name_error", + "translation": "समूह का नाम पहले से मौजूद है" + }, + { + "id": "api.job.unable_to_download_job", + "translation": "इस जौब को डाउनलोड करने में असमर्थ" + }, + { + "id": "api.io_error", + "translation": "इनपुट/आउटपुट त्रुटि" + }, + { + "id": "api.incoming_webhook.invalid_username.app_error", + "translation": "अमान्य उपयोगकर्ता नाम।" + }, + { + "id": "api.incoming_webhook.disabled.app_error", + "translation": "आने वाले वेबहुक को सिस्टम व्यवस्थापक द्वारा अक्षम कर दिया गया है।" + }, + { + "id": "api.image.get.app_error", + "translation": "अनुरोधित इमेज url को पार्स नहीं किया जा सकता है।" + }, + { + "id": "api.file.upload_file.too_large_detailed.app_error", + "translation": "फ़ाइल अपलोड करने में असमर्थ {{.Filename}}. {{.Length}} बाइट अधिकतम अनुमत {{.Limit}} बाइट से अधिक है।" + }, + { + "id": "api.file.upload_file.storage.app_error", + "translation": "फाइल अपलोड करने में असमर्थ। इमेज संग्रहण कॉन्फ़िगर नहीं किया गया है।" + }, + { + "id": "api.file.upload_file.read_request.app_error", + "translation": "फ़ाइल अपलोड करने में असमर्थ। अनुरोध डेटा पढ़ने या पार्स करने में त्रुटि।" + }, + { + "id": "api.file.upload_file.read_form_value.app_error", + "translation": "फ़ाइल अपलोड करने में असमर्थ। {{.Formname}} का मान पढ़ने में त्रुटि।" + }, + { + "id": "api.file.upload_file.multiple_channel_ids.app_error", + "translation": "फ़ाइल अपलोड करने में असमर्थ। अनेक परस्पर विरोधी channel_ids." + }, + { + "id": "api.file.upload_file.large_image_detailed.app_error", + "translation": "{{.Filename}} आयाम ({{.Width}} गुणा {{.Height}} पिक्सल) सीमा से अधिक हैं।" + }, + { + "id": "api.file.upload_file.large_image.app_error", + "translation": "अधिकतम आयामों से ऊपर की फ़ाइल अपलोड नहीं की जा सकी: {{.Filename}}" + }, + { + "id": "api.file.upload_file.incorrect_number_of_client_ids.app_error", + "translation": "फ़ाइल अपलोड करने में असमर्थ। {{.NumFiles}} फ़ाइलों के लिए {{.NumClientIds}} client_ids हैं।" + }, + { + "id": "api.file.upload_file.incorrect_channelId.app_error", + "translation": "फ़ाइल अपलोड करने में असमर्थ. गलत चैनल आईडी: {{.channelId}}" + }, + { + "id": "api.file.test_connection_s3_bucket_does_not_exist.app_error", + "translation": "सुनिश्चित करें कि आपका Amazon S3 बकेट उपलब्ध है, और अपनी बकेट अनुमतियों को सत्यापित करें।" + }, + { + "id": "api.file.test_connection_s3_auth.app_error", + "translation": "S3 से कनेक्ट करने में असमर्थ। अपने Amazon S3 कनेक्शन प्राधिकरण मापदंडों और प्रमाणीकरण सेटिंग्स को सत्यापित करें।" + }, + { + "id": "api.email_batching.send_batched_email_notification.subject", + "translation": { + "one": "[{{.SiteName}}] {{.Month}} {{.Day}}, {{.Year}} के लिए नया नोटिफिकेशन", + "other": "[{{.SiteName}}] {{.Month}} {{.Day}}, {{.Year}} के लिए नए नोटिफिकेशनस" + } + }, + { + "id": "api.context.json_encoding.app_error", + "translation": "JSON को एन्कोड करने में त्रुटि।" + }, + { + "id": "api.context.invitation_expired.error", + "translation": "आमंत्रण समाप्त हो गया है।" + }, + { + "id": "api.context.invalid_url_param.app_error", + "translation": "अनुरोध URL में अमान्य या अनुपलब्ध {{.Name}} पैरामीटर।" + }, + { + "id": "api.context.invalid_token.error", + "translation": "अमान्य सत्र टोकन = {{.Token}}, त्रुटि = {{.Error}}" + }, + { + "id": "api.context.invalid_param.app_error", + "translation": "अमान्य {{.Name}} पैरामीटर।" + }, + { + "id": "api.context.invalid_body_param.app_error", + "translation": "अनुरोध के मुख्य भाग में अमान्य या अनुपलब्ध {{.Name}}।" + }, + { + "id": "api.context.404.app_error", + "translation": "क्षमा करें, हमें पेज नहीं मिला।" + }, + { + "id": "api.config.update_config.restricted_merge.app_error", + "translation": "दिए गए कॉन्फ़िगरेशन को मर्ज करने में विफल।" + }, + { + "id": "api.config.update_config.diff.app_error", + "translation": "कॉन्फ़िगरेशन को अलग करने में विफल" + }, + { + "id": "api.config.reload_config.app_error", + "translation": "कॉन्फ़िगरेशन को पुनः लोड करने में विफल।" + }, + { + "id": "api.config.patch_config.restricted_merge.app_error", + "translation": "दिए गए कॉन्फ़िगरेशन को मर्ज करने में विफल।" + }, + { + "id": "api.config.migrate_config.app_error", + "translation": "कॉन्फिग स्टोर को माइग्रेट करने में विफल।" + }, + { + "id": "api.config.get_config.restricted_merge.app_error", + "translation": "दिए गए कॉन्फ़िगरेशन को मर्ज करने में विफल।" + }, + { + "id": "api.config.client.old_format.app_error", + "translation": "क्लाइंट कॉन्फ़िगरेशन के लिए नया स्वरूप अभी समर्थित नहीं है। कृपया क्वेरी स्ट्रिंग में प्रारूप = पुराना निर्दिष्ट करें।" + }, + { + "id": "api.command_shrug.hint", + "translation": "[मैसेज]" + }, + { + "id": "api.command_shrug.desc", + "translation": "आपके मैसेज में ¯\\_(ツ)_/¯ जोड़ता है" + }, + { + "id": "api.command_shortcuts.unsupported.app_error", + "translation": "आपके डिवाइस पर शॉर्टकट कमांड समर्थित नहीं है।" + }, + { + "id": "api.command_shortcuts.name", + "translation": "शॉर्टकट" + }, + { + "id": "api.command_shortcuts.desc", + "translation": "कीबोर्ड शॉर्टकट की सूची प्रदर्शित करता है" + }, + { + "id": "api.command_share.unshare_channel.help", + "translation": "वर्तमान चैनल को अनशेयर करता है" + }, + { + "id": "api.command_share.unknown_action", + "translation": "अज्ञात क्रिया `{{.Action}}`। उपलब्ध कार्रवाइयाँ: {{.Actions}}" + }, + { + "id": "api.command_share.shared_channel_unshare.error", + "translation": "इस चैनल को साझा नहीं किया जा सकता: {{.Error}}." + }, + { + "id": "api.command_share.share_read_only.hint", + "translation": "[केवल पढ़ने के लिए] - 'वाई' या 'एन'। 'एन' के लिए डिफ़ॉल्ट" + }, + { + "id": "api.command_share.remote_table_header", + "translation": "| सुरक्षित कनेक्शन | साइट यूआरएल | केवल पढ़ने के लिए | आमंत्रण स्वीकृत | ऑनलाइन | अंतिम समन्वयन |" + }, + { + "id": "ent.compliance.licence_disable.app_error", + "translation": "अनुपालन कार्यक्षमता वर्तमान लाइसेंस द्वारा अक्षम की गई है। अपने एंटरप्राइज़ लाइसेंस को अपग्रेड करने के बारे में कृपया अपने सिस्टम व्यवस्थापक से संपर्क करें।" + }, + { + "id": "ent.compliance.global_relay.open_temporary_file.appError", + "translation": "अस्थायी निर्यात फ़ाइल खोलने में असमर्थ." + }, + { + "id": "ent.compliance.global_relay.attachments_removed.appError", + "translation": "अपलोड की गई फ़ाइल को ग्लोबल रिले निर्यात से निकाल दिया गया था क्योंकि यह भेजने के लिए बहुत बड़ी थी।" + }, + { + "id": "ent.compliance.csv.warning.appError", + "translation": "चेतावनी फ़ाइल बनाने में असमर्थ।" + }, + { + "id": "ent.compliance.csv.post.export.appError", + "translation": "पोस्ट निर्यात करने में असमर्थ." + }, + { + "id": "ent.compliance.csv.metadata.json.zipfile.appError", + "translation": "ज़िप फ़ाइल बनाने में असमर्थ" + }, + { + "id": "ent.compliance.csv.attachment.export.appError", + "translation": "CSV निर्यात में अटैचमेंट जोड़ने में असमर्थ." + }, + { + "id": "ent.compliance.csv.attachment.copy.appError", + "translation": "ज़िप फ़ाइल में अटैचमेंट की प्रतिलिपि बनाने में असमर्थ।" + }, + { + "id": "ent.compliance.bad_export_type.appError", + "translation": "अज्ञात आउटपुट स्वरूप {{.ExportType}}" + }, + { + "id": "ent.cluster.timeout.error", + "translation": "क्लस्टर प्रतिक्रिया की प्रतीक्षा में समय समाप्त" + }, + { + "id": "ent.cluster.save_config.error", + "translation": "उच्च उपलब्धता सक्षम होने पर सिस्टम कंसोल केवल-पढ़ने के लिए सेट किया जाता है जब तक कि कॉन्फ़िगरेशन फ़ाइल में ReadOnlyConfig अक्षम न हो।" + }, + { + "id": "ent.cluster.config_changed.info", + "translation": "id={{ .id }} के लिए क्लस्टर कॉन्फ़िगरेशन बदल गया है। क्लस्टर अस्थिर हो सकता है और पुनरारंभ की आवश्यकता है। यह सुनिश्चित करने के लिए कि क्लस्टर सही ढंग से कॉन्फ़िगर किया गया है, आपको तुरंत रोलिंग पुनरारंभ करना चाहिए।" + }, + { + "id": "bleveengine.indexer.index_batch.nothing_left_to_index.error", + "translation": "सभी निकायों के पूर्ण होने पर एक नए बैच को अनुक्रमित करने का प्रयास किया जा रहा है।" + }, + { + "id": "bleveengine.indexer.do_job.get_oldest_entity.error", + "translation": "सबसे पुरानी इकाई (उपयोगकर्ता, चैनल या पोस्ट) को डेटाबेस से पुनर्प्राप्त नहीं किया जा सका।" + }, + { + "id": "bleveengine.indexer.do_job.bulk_index_users.batch_error", + "translation": "उपयोगकर्ता बैच को अनुक्रमित करने में विफल।" + }, + { + "id": "bleveengine.indexer.do_job.bulk_index_posts.batch_error", + "translation": "पोस्ट बैच को अनुक्रमित करने में विफल।" + }, + { + "id": "bleveengine.indexer.do_job.bulk_index_channels.batch_error", + "translation": "चैनल बैच को अनुक्रमित करने में विफल।" + }, + { + "id": "bleveengine.index_user.error", + "translation": "उपयोगकर्ता को अनुक्रमित करने में विफल।" + }, + { + "id": "bleveengine.index_post.error", + "translation": "पोस्ट को अनुक्रमित करने में विफल." + }, + { + "id": "bleveengine.index_channel.error", + "translation": "चैनल को अनुक्रमित करने में विफल." + }, + { + "id": "bleveengine.delete_user_posts.error", + "translation": "उपयोगकर्ता पोस्ट हटाने में विफल" + }, + { + "id": "bleveengine.delete_user.error", + "translation": "उपयोगकर्ता को हटाने में विफल।" + }, + { + "id": "bleveengine.delete_post.error", + "translation": "पोस्ट हटाने में विफल." + }, + { + "id": "bleveengine.delete_channel_posts.error", + "translation": "चैनल पोस्ट हटाने में विफल" + }, + { + "id": "bleveengine.delete_channel.error", + "translation": "चैनल हटाने में विफल." + }, + { + "id": "app.webhooks.update_incoming.app_error", + "translation": "इनकमिंगवेबहुक को अपडेट करने में असमर्थ।" + }, + { + "id": "app.webhooks.save_outgoing.override.app_error", + "translation": "आप किसी मौजूदा आउटगोइंग वेबहुक को अधिलेखित नहीं कर सकते हैं।" + }, + { + "id": "app.webhooks.save_outgoing.app_error", + "translation": "आउटगोइंगवेबहुक को सहेजने में असमर्थ।" + }, + { + "id": "api.server.warn_metric.bot_response.start_trial_failure.message", + "translation": "परीक्षण लाइसेंस पुनर्प्राप्त नहीं किया जा सका। लाइसेंस का अनुरोध करने के लिए https://mattermost.com/trial/ पर जाएं।" + }, + { + "id": "api.server.warn_metric.bot_response.notification_success.message", + "translation": "मैटरमॉस्ट से संपर्क करने के लिए धन्यवाद। हम जल्द ही आपसे संपर्क करेंगे।" + }, + { + "id": "api.preference.delete_preferences.delete.app_error", + "translation": "उपयोक्ता वरीयताएँ मिटाने में असमर्थ।" + }, + { + "id": "api.post.send_notifications_and_forget.push_general_message", + "translation": " एक मैसेज पोस्ट किया।" + }, + { + "id": "api.post.link_preview_disabled.app_error", + "translation": "लिंक प्रिवीउस को सिस्टम व्यवस्थापक द्वारा अक्षम कर दिया गया है।" + }, + { + "id": "api.post.get_message_for_notification.files_sent", + "translation": { + "one": "{{.Count}} फ़ाइल भेजी गई: {{.Filenames}}", + "other": "{{.Count}} फ़ाइलें भेजी गईं: {{.Filenames}}" + } + }, + { + "id": "api.post.deduplicate_create_post.pending", + "translation": "अस्वीकृत पोस्ट क्योंकि कोई अन्य क्लाइंट वही अनुरोध कर रहा है।" + }, + { + "id": "api.post.create_webhook_post.creating.app_error", + "translation": "पोस्ट बनाने में त्रुटि." + }, + { + "id": "api.oauth.get_access_token.credentials.app_error", + "translation": "अमान्य_क्लाइंट: अमान्य क्लाइंट क्रेडेंशियल।" + }, + { + "id": "app.oauth.permanent_delete_auth_data_by_user.app_error", + "translation": "प्राधिकरण कोड निकालने में असमर्थ." + }, + { + "id": "app.oauth.get_apps.find.app_error", + "translation": "OAuth2 ऐप्स ढूंढते समय एक त्रुटि हुई।" + }, + { + "id": "app.oauth.get_app_by_user.find.app_error", + "translation": "कोई भी मौजूदा ऐप्स खोजने में असमर्थ।" + }, + { + "id": "app.oauth.get_app.finding.app_error", + "translation": "हमें ऐप खोजने में एक त्रुटि का सामना करना पड़ा।" + }, + { + "id": "app.oauth.get_app.find.app_error", + "translation": "अनुरोधित ऐप खोजने में असमर्थ।" + }, + { + "id": "app.oauth.get_access_data_by_user_for_app.app_error", + "translation": "हमें सभी एक्सेस टोकन खोजने में एक त्रुटि का सामना करना पड़ा।" + }, + { + "id": "app.oauth.delete_app.app_error", + "translation": "OAuth2 ऐप को हटाते समय एक त्रुटि हुई।" + }, + { + "id": "app.notification.subject.notification.full", + "translation": "[{{ .SiteName }}] {{.TeamName}} में {{.Month}} {{.Day}}, {{.Year}} को नोटिफिकेशन" + }, + { + "id": "app.notification.subject.group_message.generic", + "translation": "[{{ .SiteName }}] नया ग्रूप मैसेज {{.Month}} {{.Day}}, {{.Year}} को" + }, + { + "id": "model.config.is_valid.max_channels.app_error", + "translation": "टीम सेटिंग के लिए प्रति टीम अमान्य अधिकतम चैनल। अवश्य ही एक धनात्मक संख्या होनी चाहिए।" + }, + { + "id": "api.user.login.not_verified.app_error", + "translation": "लॉगिन विफल हुआ क्योंकि ईमेल सत्यापित नहीं किया गया है।" + }, + { + "id": "api.user.login.invalid_credentials_username", + "translation": "एक मान्य उपयोगकर्ता नाम और/या पासवर्ड दर्ज करें।" + }, + { + "id": "api.user.login.invalid_credentials_sso", + "translation": "एक मान्य ईमेल या उपयोगकर्ता नाम और/या पासवर्ड दर्ज करें, या किसी अन्य विधि का उपयोग करके साइन इन करें।" + }, + { + "id": "api.user.login.invalid_credentials_email_username", + "translation": "एक मान्य ईमेल या उपयोगकर्ता नाम और/या पासवर्ड दर्ज करें।" + }, + { + "id": "api.user.login.invalid_credentials_email", + "translation": "एक मान्य ईमेल और/या पासवर्ड दर्ज करें" + }, + { + "id": "api.user.login.inactive.app_error", + "translation": "लॉगिन विफल हुआ क्योंकि आपका अकाऊंत निष्क्रिय कर दिया गया है। कृपया किसी व्यवस्थापक से संपर्क करें." + }, + { + "id": "api.user.login.guest_accounts.license.error", + "translation": "आपका लाइसेंस अतिथि अकाऊंत का समर्थन नहीं करता" + }, + { + "id": "api.upload.upload_data.multipart_error", + "translation": "मल्टीपार्ट डेटा को संसाधित करने में विफल।" + }, + { + "id": "api.upload.upload_data.invalid_content_length", + "translation": "अमान्य सामग्री-लंबाई।" + }, + { + "id": "api.upload.get_upload.forbidden.app_error", + "translation": "अपलोड प्राप्त करने में विफल।" + }, + { + "id": "api.upgrade_to_enterprise_status.signature.app_error", + "translation": "मैटरमॉस्ट एंटरप्राइज़ संस्करण में अपग्रेड करने में असमर्थ था। डाउनलोड की गई बाइनरी फ़ाइल का डिजिटल हस्ताक्षर सत्यापित नहीं किया जा सका।" + }, + { + "id": "api.upgrade_to_enterprise_status.app_error", + "translation": "मैटरमॉस्ट एंटरप्राइज़ संस्करण में अपग्रेड करने में असमर्थ था।" + }, + { + "id": "api.upgrade_to_enterprise.system_not_supported.app_error", + "translation": "मैटरमॉस्ट एंटरप्राइज़ संस्करण में अपग्रेड करने में असमर्थ था। यह सुविधा केवल x86-64 आर्किटेक्चर वाले Linux सिस्टम पर काम करेगी।" + }, + { + "id": "api.upgrade_to_enterprise.invalid-user.app_error", + "translation": "मैटरमॉस्ट एंटरप्राइज़ संस्करण में अपग्रेड करने में असमर्थ था। मैटरमोस्ट सिस्टम उपयोगकर्ता {{.MattermostUsername}} के पास आवश्यक बाइनरी फ़ाइल तक लेखन पहुंच नहीं है। एक सिस्टम व्यवस्थापक सर्वर पर निम्न आदेश निष्पादित करके फ़ाइल अनुमतियों को अद्यतन कर सकता है जहां मैटरमॉस्ट स्थापित है:\n\n```\nचुना हुआ {{.MattermostUsername}} \"{{.Path}}\"\n```\n\nफ़ाइल अनुमतियों को बदलने के बाद, मैटरमोस्ट को फिर से अपग्रेड करने का प्रयास करें। जब आपने अपग्रेड और पुनरारंभ किया है, तो मूल बाइनरी फ़ाइल अनुमतियों को पुनर्स्थापित करना याद रखें:\n\n```\nचुना हुआ {{.FileUsername}} \"{{.Path}}\"\n```" + }, + { + "id": "api.upgrade_to_enterprise.invalid-user-and-permission.app_error", + "translation": "मैटरमॉस्ट एंटरप्राइज़ संस्करण में अपग्रेड करने में असमर्थ था। मैटरमोस्ट सिस्टम उपयोगकर्ता {{.MattermostUsername}} के पास आवश्यक बाइनरी फ़ाइल तक लेखन पहुंच नहीं है। एक सिस्टम व्यवस्थापक सर्वर पर निम्न आदेश निष्पादित करके फ़ाइल अनुमतियों को अद्यतन कर सकता है जहां मैटरमॉस्ट स्थापित है:\n\n```\nचुना हुआ {{.MattermostUsername}} \"{{.Path}}\"\nchmod +w \"{{.Path}}\"\n```\n\nफ़ाइल अनुमतियों को बदलने के बाद, मैटरमोस्ट को फिर से अपग्रेड करने का प्रयास करें। जब आपने अपग्रेड और पुनरारंभ किया है, तो मूल बाइनरी फ़ाइल अनुमतियों को पुनर्स्थापित करना याद रखें:\n\n```\nचुना हुआ {{.FileUsername}} \"{{.Path}}\"\nchmod -w \"{{.Path}}\"\n```" + }, + { + "id": "api.upgrade_to_enterprise.invalid-permission.app_error", + "translation": "मैटरमॉस्ट एंटरप्राइज़ संस्करण में अपग्रेड करने में असमर्थ था। मैटरमोस्ट सिस्टम उपयोगकर्ता {{.MattermostUsername}} के पास आवश्यक बाइनरी फ़ाइल तक लेखन पहुंच नहीं है। एक सिस्टम व्यवस्थापक सर्वर पर निम्न आदेश निष्पादित करके फ़ाइल अनुमतियों को अद्यतन कर सकता है जहां मैटरमॉस्ट स्थापित है:\n\n```\nchmod +w \"{{.Path}}\"\n```\n\nफ़ाइल अनुमतियों को बदलने के बाद, मैटरमोस्ट को फिर से अपग्रेड करने का प्रयास करें। जब आपने अपग्रेड और पुनरारंभ किया है, तो मूल बाइनरी फ़ाइल अनुमतियों को पुनर्स्थापित करना याद रखें:\n\n```\nchmod -w \"{{.Path}}\"\n```" + }, + { + "id": "api.upgrade_to_enterprise.generic_error.app_error", + "translation": "मैटरमॉस्ट एंटरप्राइज़ संस्करण में अपग्रेड करने में असमर्थ था।" + }, + { + "id": "api.upgrade_to_enterprise.app_error", + "translation": "मैटरमॉस्ट एंटरप्राइज़ संस्करण में अपग्रेड पहले से चल रहा है।" + }, + { + "id": "api.upgrade_to_enterprise.already-enterprise.app_error", + "translation": "आप अपग्रेड नहीं कर सकते क्योंकि आप पहले से ही मैटरमॉस्ट एंटरप्राइज संस्करण चला रहे हैं।" + }, + { + "id": "api.upgrade_to_enterprise.already-done.app_error", + "translation": "आप पहले ही मैटरमॉस्ट एंटरप्राइज संस्करण में अपग्रेड कर चुके हैं। कृपया अपग्रेड समाप्त करने के लिए सर्वर को पुनरारंभ करें।" + }, + { + "id": "api.unmarshal_error", + "translation": "अनमार्शल करने में विफल।" + }, + { + "id": "api.templates.mfa_activated_body.title", + "translation": "बहु-कारक प्रमाणीकरण जोड़ा गया था" + }, + { + "id": "api.templates.mfa_activated_body.info", + "translation": "{{ .SiteURL }} पर आपके खाते में बहु-कारक प्रमाणीकरण जोड़ दिया गया है।" + }, + { + "id": "api.templates.deactivate_subject", + "translation": "[{{ .SiteName }}] {{ .ServerURL }} पर आपका अकाऊंत निष्क्रिय कर दिया गया है" + }, + { + "id": "api.templates.deactivate_body.warning", + "translation": "यदि यह परिवर्तन आपके द्वारा शुरू नहीं किया गया था या आप अपने अकाऊंत को पुनः सक्रिय करना चाहते हैं, तो अपने सिस्टम व्यवस्थापक से संपर्क करें।" + }, + { + "id": "api.templates.deactivate_body.title", + "translation": "आपका अकाऊंत {{ .ServerURL }} पर निष्क्रिय कर दिया गया है" + }, + { + "id": "api.templates.deactivate_body.info", + "translation": "आपने अपना अकाऊंत {{ .SiteURL }} पर निष्क्रिय कर दिया है।" + }, + { + "id": "api.templates.cloud_welcome_email.mm_apps", + "translation": "मोबाइल और डेस्कटॉप ऐप्स" + }, + { + "id": "api.templates.cloud_welcome_email.invite_sub_info", + "translation": "अपने सदस्यों को {{.WorkSpace}} में शामिल होने के लिए आमंत्रित करने के लिए इस लिंक को साझा करें:" + }, + { + "id": "api.templates.cloud_welcome_email.invite_info", + "translation": "लोगों को अपने कार्यक्षेत्र में आमंत्रित करें" + }, + { + "id": "api.templates.cloud_welcome_email.info2", + "translation": "भविष्य में उपयोग के लिए अपने लिंक को सहेजना या बुकमार्क करना सुनिश्चित करें।" + }, + { + "id": "api.templates.cloud_welcome_email.info", + "translation": "बनाने के लिए धन्यवाद " + }, + { + "id": "api.templates.cloud_welcome_email.download_mm_info", + "translation": "मैटरमोस्ट ऐप डाउनलोड करें" + }, + { + "id": "api.templates.cloud_welcome_email.button", + "translation": "ओपन मैटरमोस्ट" + }, + { + "id": "api.templates.cloud_welcome_email.app_market_place", + "translation": "ऐप मार्केटप्लेस।" + }, + { + "id": "api.templates.cloud_welcome_email.add_apps_sub_info", + "translation": "Github, Google Calendar और Chrome जैसे टूल के साथ अपने काम को सुव्यवस्थित करें। हमारे पास मौजूद सभी एकीकरणों का अन्वेषण करें" + }, + { + "id": "api.templates.cloud_welcome_email.add_apps_info", + "translation": "अपने कार्यक्षेत्र में ऐप्स जोड़ें" + }, + { + "id": "api.user.oauth_to_email.not_available.app_error", + "translation": "प्रमाणीकरण स्थानांतरण कॉन्फ़िगर नहीं किया गया है या इस सर्वर पर उपलब्ध नहीं है।" + }, + { + "id": "api.command_leave.name", + "translation": "लीव" + }, + { + "id": "api.command_leave.fail.app_error", + "translation": "चैनल छोड़ते समय एक त्रुटि हुई।" + }, + { + "id": "api.command_leave.desc", + "translation": "वर्तमान चैनल छोड़ें" + }, + { + "id": "api.command_kick.name", + "translation": "किक" + }, + { + "id": "api.command_join.name", + "translation": "जुडे" + }, + { + "id": "api.command_join.missing.app_error", + "translation": "चैनल ढूंढ़ने में असमर्थ." + }, + { + "id": "api.command_join.list.app_error", + "translation": "चैनलस लिस्ट करते समय एक त्रुटि हुई।" + }, + { + "id": "api.command_join.hint", + "translation": "~[channel]" + }, + { + "id": "api.command_join.fail.app_error", + "translation": "चैनल से जुड़ते समय एक त्रुटि हुई।" + }, + { + "id": "api.command_join.desc", + "translation": "ओपन चैनल से जुड़ें" + }, + { + "id": "api.command_invite_people.permission.app_error", + "translation": "आपको इस सर्वर पर नए उपयोगकर्ताओं को आमंत्रित करने की अनुमति नहीं है।" + }, + { + "id": "api.command_invite.user_not_in_team.app_error", + "translation": "@{{.Username}} टीम का सदस्य नहीं है।" + }, + { + "id": "api.command_invite.user_already_in_channel.app_error", + "translation": "{{.User}} पहले से ही चैनल में है।" + }, + { + "id": "api.command_invite.success", + "translation": "{{.User}} को {{.Channel}} चैनल में जोड़ा गया।" + }, + { + "id": "api.command_invite.private_channel.app_error", + "translation": "चैनल {{.Channel}} नहीं मिला। चैनलों की पहचान करने के लिए कृपया चैनल हैंडल का उपयोग करें।" + }, + { + "id": "api.command_invite.permission.app_error", + "translation": "आपके पास {{.User}} को {{.Channel}} में जोड़ने के लिए पर्याप्त अनुमतियां नहीं हैं।" + }, + { + "id": "api.command_invite.name", + "translation": "आमंत्रण" + }, + { + "id": "api.command_invite.missing_user.app_error", + "translation": "हमें उपयोगकर्ता नहीं मिला. हो सकता है कि उन्हें सिस्टम व्यवस्थापक द्वारा निष्क्रिय कर दिया गया हो।" + }, + { + "id": "api.command_invite.missing_message.app_error", + "translation": "उपयोगकर्ता नाम और चैनल गुम है." + }, + { + "id": "api.command_invite.hint", + "translation": "@[username] ~[channel]" + }, + { + "id": "api.command_invite.group_constrained_user_denied", + "translation": "इस चैनल का प्रबंधन समूहों द्वारा किया जाता है। यह उपयोगकर्ता उस समूह का हिस्सा नहीं है जो इस चैनल से समन्वयित है।" + }, + { + "id": "api.command_invite.fail.app_error", + "translation": "चैनल में शामिल होते समय एक त्रुटि हुई।" + }, + { + "id": "api.command_invite.directchannel.app_error", + "translation": "आप किसी को डायरैक्ट मैसेज चैनल में नहीं जोड़ सकते।" + }, + { + "id": "api.command_invite.channel.error", + "translation": "चैनल {{.Channel}} नहीं मिला। चैनलों की पहचान करने के लिए कृपया [चैनल हैंडल](https://docs.mattermost.com/messaging/managing-channels.html#naming-a-channel) का उपयोग करें।" + }, + { + "id": "api.command_invite.channel.app_error", + "translation": "वर्तमान चैनल को पुनः प्राप्त करने में त्रुटि।" + }, + { + "id": "api.command_help.name", + "translation": "सहायता" + }, + { + "id": "api.command_help.desc", + "translation": "मैटरमोस्ट सहायता संदेश दिखाएं" + }, + { + "id": "api.command_groupmsg.permission.app_error", + "translation": "आपके पास नया ग्रूप मैसेज बनाने के लिए उपयुक्त अनुमतियाँ नहीं हैं।" + }, + { + "id": "api.command_groupmsg.name", + "translation": "मैसेज" + }, + { + "id": "api.command_groupmsg.min_users.app_error", + "translation": "ग्रूप मैसेज कम से कम {{.MinUsers}} उपयोगकर्ताओं तक सीमित हैं।" + }, + { + "id": "api.command_groupmsg.max_users.app_error", + "translation": "ग्रूप मैसेज अधिकतम {{.MaxUsers}} उपयोगकर्ताओं तक सीमित हैं।" + }, + { + "id": "api.command_groupmsg.invalid_user.app_error", + "translation": { + "one": "उपयोगकर्ता ढूंढने में असमर्थ: {{.Users}}", + "other": "उपयोगकर्ताओं को ढूंढने में असमर्थ: {{.Users}}" + } + }, + { + "id": "api.command_groupmsg.hint", + "translation": "@[username1],@[username2] 'message'" + }, + { + "id": "api.command_groupmsg.group_fail.app_error", + "translation": "ग्रूप मैसेज बनाते समय एक त्रुटि हुई।" + }, + { + "id": "api.command_groupmsg.fail.app_error", + "translation": "उपयोगकर्ताओं को मैसेज भेजते समय एक त्रुटि हुई।" + }, + { + "id": "api.command_groupmsg.desc", + "translation": "निर्दिष्ट उपयोगकर्ताओं को एक समूह मैसेज भेजता है" + }, + { + "id": "api.command_expand_collapse.fail.app_error", + "translation": "प्रिवीउस विस्तृत करते समय एक त्रुटि हुई." + }, + { + "id": "api.command_expand.success", + "translation": "इमेज लिंक अब डिफ़ॉल्ट रूप से विस्तृत होते हैं" + }, + { + "id": "api.command_expand.name", + "translation": "विस्तार" + }, + { + "id": "api.command_expand.desc", + "translation": "इमेज प्रिवीउस का औटो कोलैप्सिंग बंद करें" + }, + { + "id": "api.command_echo.message.app_error", + "translation": "/echo कमांड के साथ एक मैसेज अवश्य दिया जाना चाहिए।" + }, + { + "id": "api.command_echo.hint", + "translation": "'मैसेज' [सेकंड में देरी]" + }, + { + "id": "api.command_echo.delay.app_error", + "translation": "विलंब 10000 सेकंड से कम होना चाहिए।" + }, + { + "id": "api.command_dnd.success", + "translation": "डू नॉट डिस्टर्ब सक्षम है। डू नॉट डिस्टर्ब बंद होने तक आपको डेस्कटॉप या मोबाइल पुश नोटिफिकेशन प्राप्त नहीं होंगे।" + }, + { + "id": "api.command_dnd.name", + "translation": "डीएनडी" + }, + { + "id": "api.command_dnd.desc", + "translation": "परेशान न करें डेस्कटॉप और मोबाइल पुश सूचनाओं को अक्षम करता है।" + }, + { + "id": "api.command_collapse.success", + "translation": "इमेज लिंक अब डिफ़ॉल्ट रूप से कोलैप्स हो जाते हैं" + }, + { + "id": "api.command_collapse.name", + "translation": "कोलैप्स" + }, + { + "id": "api.command.command_post.forbidden.app_error", + "translation": "निर्दिष्ट उपयोगकर्ता निर्दिष्ट चैनल का सदस्य नहीं है।" + }, + { + "id": "api.command.admin_only.app_error", + "translation": "इंटीग्रेश्न्स केवल व्यवस्थापकों तक ही सीमित है।" + }, + { + "id": "api.cloud.request_error", + "translation": "CWS को अनुरोध संसाधित करने में त्रुटि।" + }, + { + "id": "api.cloud.license_error", + "translation": "आपका लाइसेंस क्लाउड अनुरोधों का समर्थन नहीं करता है।" + }, + { + "id": "api.cloud.cws_webhook_event_missing_error", + "translation": "वैबहुक ईवेंट को हैंडल नहीं किया गया था। या तो यह गायब है या यह मान्य नहीं है।" + }, + { + "id": "api.cloud.app_error", + "translation": "क्लाउड एपीआई अनुरोध के दौरान आंतरिक त्रुटि।" + }, + { + "id": "api.channel.update_team_member_roles.scheme_role.app_error", + "translation": "प्रदान की गई भूमिका एक स्कीम द्वारा प्रबंधित की जाती है और इसलिए इसे सीधे टीम सदस्य पर लागू नहीं किया जा सकता है।" + }, + { + "id": "api.channel.update_team_member_roles.changing_guest_role.app_error", + "translation": "अमान्य टीम सदस्य अपडेट: आप अतिथि भूमिका को मैन्युअल रूप से जोड़ या हटा नहीं सकते हैं।" + }, + { + "id": "api.channel.update_channel_scheme.scheme_scope.error", + "translation": "स्कीम को चैनल पर सेट करने में असमर्थ क्योंकि आपूर्ति की गई स्कीम एक चैनल स्कीम नहीं है।" + }, + { + "id": "api.channel.update_channel_member_roles.guest_and_user.app_error", + "translation": "अमान्य चैनल सदस्य अपडेट: उपयोगकर्ता को अतिथि या उपयोगकर्ता होना चाहिए, लेकिन दोनों नहीं।" + }, + { + "id": "api.channel.restore_channel.unarchived", + "translation": "{{.Username}} ने चैनल को असंग्रहित कर दिया।" + }, + { + "id": "api.channel.rename_channel.cant_rename_group_messages.app_error", + "translation": "आप ग्रूप मैसेज चैनल का नाम नहीं बदल सकते।" + }, + { + "id": "api.channel.rename_channel.cant_rename_direct_messages.app_error", + "translation": "आप डायरैक्ट मैसेज चैनल का नाम नहीं बदल सकते।" + }, + { + "id": "api.channel.move_channel.type.invalid", + "translation": "डायरैक्ट या ग्रूप मैसेज चैनलों को स्थानांतरित करने में असमर्थ" + }, + { + "id": "api.channel.leave.direct.app_error", + "translation": "एक डायरैक्ट मैसेज चैनल छोड़ने में असमर्थ।" + }, + { + "id": "api.channel.delete_channel.type.invalid", + "translation": "डायरैक्ट या ग्रूप मैसेज चैनलों को हटाने में असमर्थ" + }, + { + "id": "api.channel.create_direct_channel.invalid_user.app_error", + "translation": "डायरैक्ट चैनल बनाने के लिए अमान्य उपयोगकर्ता आईडी." + }, + { + "id": "api.channel.create_default_channels.town_square", + "translation": "टाउन स्कवायर" + }, + { + "id": "api.channel.create_default_channels.off_topic", + "translation": "ऑफ-टौपिक" + }, + { + "id": "api.user.login.use_auth_service.app_error", + "translation": "कृपया {{.AuthService}} का उपयोग करके साइन इन करें।" + }, + { + "id": "model.group.name.invalid_length.app_error", + "translation": "नाम 1 से 64 लोअरकेस अक्षरांकीय वर्णों का होना चाहिए।" + }, + { + "id": "model.file_info.is_valid.user_id.app_error", + "translation": "user_id के लिए अमान्य मान." + }, + { + "id": "model.file_info.is_valid.update_at.app_error", + "translation": "update_at के लिए अमान्य मान।" + }, + { + "id": "model.file_info.is_valid.post_id.app_error", + "translation": "post_id के लिए अमान्य मान।" + }, + { + "id": "model.file_info.is_valid.path.app_error", + "translation": "पथ के लिए अमान्य मान." + }, + { + "id": "model.file_info.is_valid.id.app_error", + "translation": "आईडी के लिए अमान्य मान." + }, + { + "id": "model.file_info.is_valid.create_at.app_error", + "translation": "create_at के लिए अमान्य मान।" + }, + { + "id": "model.file_info.get.gif.app_error", + "translation": "जीआईएफ डीकोड नहीं कर सका।" + }, + { + "id": "model.emoji.user_id.app_error", + "translation": "अमान्य क्रिएटर आईडी." + }, + { + "id": "model.emoji.name.app_error", + "translation": "नाम 1 से 64 लोअरकेस अक्षरांकीय वर्णों का होना चाहिए।" + }, + { + "id": "model.emoji.id.app_error", + "translation": "अमान्य इमोजी आईडी." + }, + { + "id": "model.config.is_valid.websocket_url.app_error", + "translation": "वेबसोकेट URL एक मान्य URL होना चाहिए और ws:// या wss:// से शुरू होना चाहिए।" + }, + { + "id": "model.config.is_valid.webserver_security.app_error", + "translation": "वेबसर्वर कनेक्शन सुरक्षा के लिए अमान्य मान." + }, + { + "id": "model.config.is_valid.tls_overwrite_cipher.app_error", + "translation": "TLS ओवरराइट सिफर के लिए अमान्य मान पारित किया गया - कृपया मान्य मानों के लिए दस्तावेज़ीकरण देखें।" + }, + { + "id": "model.config.is_valid.tls_key_file_missing.app_error", + "translation": "TLS कुंजी फ़ाइल के लिए अमान्य मान - या तो LetsEncrypt का उपयोग करें या मौजूदा कुंजी फ़ाइल के लिए पथ सेट करें।" + }, + { + "id": "model.config.is_valid.tls_cert_file_missing.app_error", + "translation": "TLS प्रमाणपत्र फ़ाइल के लिए अमान्य मान - या तो LetsEncrypt का उपयोग करें या मौजूदा प्रमाणपत्र फ़ाइल के लिए पथ सेट करें।" + }, + { + "id": "model.config.is_valid.time_between_user_typing.app_error", + "translation": "उपयोगकर्ता टाइपिंग अपडेट के बीच का समय 1000 मिलीसेकंड से कम पर सेट नहीं किया जाना चाहिए।" + }, + { + "id": "model.config.is_valid.teammate_name_display.app_error", + "translation": "अमान्य टीममेट डिस्पले। 'full_name', 'nickname_full_name' या 'username' होना चाहिए।" + }, + { + "id": "model.config.is_valid.sql_query_timeout.app_error", + "translation": "SQL सेटिंग्स के लिए अमान्य क्वेरी टाइमआउट। अवश्य ही एक धनात्मक संख्या होनी चाहिए।" + }, + { + "id": "model.config.is_valid.sql_max_conn.app_error", + "translation": "SQL सेटिंग्स के लिए अमान्य अधिकतम खुला कनेक्शन। अवश्य ही एक धनात्मक संख्या होनी चाहिए।" + }, + { + "id": "model.config.is_valid.sql_idle.app_error", + "translation": "SQL सेटिंग्स के लिए अमान्य अधिकतम निष्क्रिय कनेक्शन। अवश्य ही एक धनात्मक संख्या होनी चाहिए।" + }, + { + "id": "model.config.is_valid.sql_driver.app_error", + "translation": "SQL सेटिंग्स के लिए अमान्य ड्राइवर नाम। 'mysql' या 'postgres' होना चाहिए।" + }, + { + "id": "model.config.is_valid.sql_data_src.app_error", + "translation": "SQL सेटिंग्स के लिए अमान्य डेटा स्रोत। सेट होना चाहिए।" + }, + { + "id": "model.config.is_valid.sql_conn_max_lifetime_milliseconds.app_error", + "translation": "SQL सेटिंग्स के लिए अमान्य कनेक्शन अधिकतम जीवनकाल। एक गैर-ऋणात्मक संख्या होनी चाहिए।" + }, + { + "id": "model.config.is_valid.sitename_length.app_error", + "translation": "साइट का नाम {{.MaxLength}} वर्णों से कम या उसके बराबर होना चाहिए।" + }, + { + "id": "model.config.is_valid.site_url_email_batching.app_error", + "translation": "जब SiteURL सेट नहीं है, तो ईमेल बैचिंग सक्षम करने में असमर्थ।" + }, + { + "id": "model.config.is_valid.site_url.app_error", + "translation": "साइट URL एक वैध URL होना चाहिए और http:// या https:// से शुरू होना चाहिए।" + }, + { + "id": "model.config.is_valid.saml_username_attribute.app_error", + "translation": "अमान्य उपयोगकर्ता नाम विशेषता। सेट होना चाहिए।" + }, + { + "id": "model.config.is_valid.saml_signature_algorithm.app_error", + "translation": "अमान्य हस्ताक्षर एल्गोरिथम।" + }, + { + "id": "api.command_share.fetch_remote.error", + "translation": "सुरक्षित कनेक्शन लाने में त्रुटि: {{.Error}}" + }, + { + "id": "api.command_share.desc", + "translation": "मौजूदा चैनल को बाहरी मैटरमोस्ट इंस्टेंस के साथ साझा करता है।" + }, + { + "id": "api.command_share.could_not_uninvite.error", + "translation": "`{{.RemoteId}}`: {{.Error}} को आमंत्रित नहीं किया जा सका" + }, + { + "id": "api.command_share.check_channel_exist.error", + "translation": "साझा चैनल मौजूद है या नहीं, इसकी जाँच करते समय त्रुटि: {{.Error}}" + }, + { + "id": "api.command_share.channel_status_id", + "translation": "चैनल आईडी का स्टेटस`{{.ChannelId}}`" + }, + { + "id": "api.command_share.channel_shared", + "translation": "यह चैनल अब साझा किया गया है।" + }, + { + "id": "api.command_share.channel_remote_id_not_exists", + "translation": "इस चैनल के लिए साझा चैनल सुरक्षित कनेक्शन `{{.RemoteId}}` मौजूद नहीं है।" + }, + { + "id": "api.command_share.channel_invite_not_home.error", + "translation": "कहीं और से शुरू होने वाले किसी साझा चैनल से सुरक्षित कनेक्शन को आमंत्रित नहीं कर सकता।" + }, + { + "id": "api.command_share.channel_invite.error", + "translation": "इस चैनल में `{{.Name}}` को आमंत्रित करने में त्रुटि: {{.Error}}" + }, + { + "id": "api.command_share.available_actions", + "translation": "उपलब्ध कार्रवाइयाँ: {{.Actions}}" + }, + { + "id": "api.command_settings.unsupported.app_error", + "translation": "आपके डिवाइस पर सेटिंग कमांड समर्थित नहीं है।" + }, + { + "id": "api.command_settings.name", + "translation": "सेटिंगस" + }, + { + "id": "api.command_settings.desc", + "translation": "सेटिंग्स संवाद खोलें" + }, + { + "id": "api.command_search.hint", + "translation": "[text]" + }, + { + "id": "api.command_remove.name", + "translation": "निकाले" + }, + { + "id": "api.command_remove.hint", + "translation": "@[username]" + }, + { + "id": "api.command_remove.direct_group.app_error", + "translation": "आप किसी को डायरैक्ट मैसेज चैनल से नहीं हटा सकते।" + }, + { + "id": "api.command_remote.remote_table_header", + "translation": "| सुरक्षित कनेक्शन | डिस्पले नाम | कनेक्शन आईडी | साइट URL | आमंत्रण स्वीकृत | ऑनलाइन | अंतिम पिंग |" + }, + { + "id": "api.command_remote.remote_add_remove.help", + "translation": "सुरक्षित कनेक्शन जोड़ें/हटाएं। उपलब्ध कार्रवाइयाँ: {{.Actions}}" + }, + { + "id": "api.command_remote.invite_summary", + "translation": "पासवर्ड के साथ बाहरी मैटरमोस्ट सिस्टम एडमिन को निम्नलिखित एईएस 256-बिट एन्क्रिप्टेड आमंत्रण भेजें। वे आमंत्रण स्वीकार करने के लिए `{{.Command}}` स्लैश कमांड का उपयोग करेंगे।\n\n```\n{{.निमंत्रण}}\n```\n\n**सुनिश्चित करें कि सुरक्षित कनेक्शन ** {{.SiteURL}} के माध्यम से आपके मैटरमोस्ट इंस्टेंस तक पहुंच सकता है" + }, + { + "id": "api.command_remote.hint", + "translation": "[action]" + }, + { + "id": "api.command_msg.permission.app_error", + "translation": "आपके पास इस उपयोगकर्ता को डायरैक्ट मैसेज भेजने के लिए उपयुक्त अनुमतियाँ नहीं हैं।" + }, + { + "id": "api.command_msg.name", + "translation": "मैसेज" + }, + { + "id": "api.command_msg.missing.app_error", + "translation": "उपयोगकर्ता को खोजने में असमर्थ।" + }, + { + "id": "api.command_msg.hint", + "translation": "@[username] 'message'" + }, + { + "id": "api.command_msg.fail.app_error", + "translation": "उपयोगकर्ता को मैसेज भेजते समय एक त्रुटि हुई।" + }, + { + "id": "api.command_msg.dm_fail.app_error", + "translation": "डायरैक्ट मैसेज बनाते समय एक त्रुटि हुई।" + }, + { + "id": "api.command_msg.desc", + "translation": "किसी उपयोगकर्ता को डायरैक्ट मैसेज भेजें" + }, + { + "id": "api.command_me.name", + "translation": "मैं" + }, + { + "id": "api.command_me.hint", + "translation": "[मैसेज]" + }, + { + "id": "api.command_me.desc", + "translation": "एक क्रिया करें" + }, + { + "id": "api.command_logout.name", + "translation": "लॉगआउट" + }, + { + "id": "api.command_logout.desc", + "translation": "मैटरमोस्ट से लॉगआउट करे" + }, + { + "id": "api.context.mfa_required.app_error", + "translation": "इस सर्वर पर बहु-कारक प्रमाणीकरण आवश्यक है।" + }, + { + "id": "api.context.get_user.app_error", + "translation": "सत्र UserID से उपयोगकर्ता प्राप्त करने में असमर्थ।" + }, + { + "id": "api.config.update_config.clear_siteurl.app_error", + "translation": "साइट URL साफ़ नहीं किया जा सकता |" + }, + { + "id": "api.config.patch_config.diff.app_error", + "translation": "कॉन्फ़िगरेशन में अंतर करने में विफल" + }, + { + "id": "api.command_share.remote_not_valid", + "translation": "आमंत्रण रद्द करने के लिए एक वैध सुरक्षित कनेक्शन आईडी निर्दिष्ट करना आवश्यक है" + }, + { + "id": "api.command_share.remote_id_invalid.error", + "translation": "सुरक्षित कनेक्शन आईडी अमान्य है: {{.Error}}" + }, + { + "id": "api.command_share.remote_already_invited", + "translation": "सुरक्षित कनेक्शन पहले ही आमंत्रित किया जा चुका है।" + }, + { + "id": "api.command_share.permission_required", + "translation": "साझा किए गए चैनलों को प्रबंधित करने के लिए आपको `{{.Permission}}` अनुमति की आवश्यकता है।" + }, + { + "id": "api.command_share.no_remote_invited", + "translation": "इस चैनल के लिए कोई सुरक्षित कनेक्शन आमंत्रित नहीं किया गया है।" + }, + { + "id": "api.command_share.name", + "translation": "शेयर-चैनल" + }, + { + "id": "api.command_share.must_specify_valid_remote", + "translation": "आमंत्रित करने के लिए एक वैध सुरक्षित कनेक्शन आईडी निर्दिष्ट करना आवश्यक है।" + }, + { + "id": "api.command_share.missing_action", + "translation": "लापता कार्रवाई। उपलब्ध कार्रवाइयाँ: {{.Actions}}" + }, + { + "id": "api.command_share.invite_remote.help", + "translation": "मौजूदा साझा चैनल के लिए बाहरी मैटरमोस्ट इंस्टेंस को आमंत्रित करता है" + }, + { + "id": "api.command_share.invalid_value.error", + "translation": "'{{.Arg}}' के लिए अमान्य मान: {{.Error}}" + }, + { + "id": "api.command_share.hint", + "translation": "[कार्य]" + }, + { + "id": "api.command_share.fetch_remote_status.error", + "translation": "सुरक्षित कनेक्शन के लिए स्थिति नहीं लाई जा सकी: {{.Error}}." + }, + { + "id": "api.command_share.channel_status.help", + "translation": "इस साझा चैनल के लिए स्थिति प्रदर्शित करता है" + }, + { + "id": "api.command_remote.missing_command", + "translation": "लापता आदेश। उपलब्ध कार्रवाइयाँ: {{.क्रियाएँ}}" + }, + { + "id": "api.server.warn_metric.bot_response.notification_failure.message", + "translation": "संदेश भेजा नहीं जा सका|" + }, + { + "id": "api.server.warn_metric.bot_response.notification_failure.body", + "translation": "कृपया हमें ईमेल करें।" + }, + { + "id": "api.channel.post_user_add_remove_message_and_forget.error", + "translation": "शामिल हों/संदेश छोड़ें पोस्ट करने में विफल" + }, + { + "id": "api.channel.post_update_channel_header_message_and_forget.post.error", + "translation": "अपडेट चैनल हेडर संदेश पोस्ट करने में विफल" + }, + { + "id": "api.channel.post_channel_privacy_message.error", + "translation": "चैनल गोपनीयता अद्यतन संदेश पोस्ट करने में विफल।" + }, + { + "id": "model.config.is_valid.elastic_search.connection_url.app_error", + "translation": "Elasticsearch ConnectionUrl सेटिंग Elasticsearch अनुक्रमण सक्षम होने पर प्रदान की जानी चाहिए।" + }, + { + "id": "model.config.is_valid.elastic_search.aggregate_posts_after_days.app_error", + "translation": "Elasticsearch AggregatePostsAfterDays सेटिंग 1 से बड़ी या उसके बराबर संख्या होनी चाहिए।" + }, + { + "id": "model.config.is_valid.display.custom_url_schemes.app_error", + "translation": "कस्टम URL स्कीम {{.Scheme}} अमान्य है। कस्टम URL स्कीमों को एक अक्षर से शुरू होना चाहिए और इसमें केवल अक्षर, संख्याएँ, प्लस (+), अवधि (.) और हाइफ़न (-) होना चाहिए।" + }, + { + "id": "api.plugin.verify_plugin.app_error", + "translation": "प्लगइन हस्ताक्षर सत्यापित करने में असमर्थ।" + }, + { + "id": "api.plugin.upload.no_file.app_error", + "translation": "मल्टीपार्ट/फॉर्म अनुरोध में अनुपलब्ध फ़ाइल।" + }, + { + "id": "api.oauth.revoke_access_token.del_token.app_error", + "translation": "डीबी से एक्सेस टोकन हटाने में त्रुटि।" + }, + { + "id": "api.oauth.revoke_access_token.del_session.app_error", + "translation": "DB से सत्र हटाने में त्रुटि।" + }, + { + "id": "api.oauth.get_access_token.refresh_token.app_error", + "translation": "अमान्य_ग्रांट: अमान्य ताज़ा टोकन।" + }, + { + "id": "api.oauth.get_access_token.missing_refresh_token.app_error", + "translation": "अमान्य_अनुरोध: ताज़ा_टोकन गुम है।" + }, + { + "id": "api.oauth.click_redirect", + "translation": "यदि आप स्वचालित रूप से पुनर्निर्देशित नहीं होते हैं, तो कृपया क्लिक करेंlink" + }, + { + "id": "api.license.request_renewal_link.cannot_renew_on_cws", + "translation": "पोर्टल पर इस लाइसेंस का नवीनीकरण संभव नहीं है" + }, + { + "id": "api.license.add_license.no_file.app_error", + "translation": "अनुरोध में 'लाइसेंस' के तहत कोई फ़ाइल नहीं।" + }, + { + "id": "api.license.add_license.invalid_count.app_error", + "translation": "कुल अद्वितीय उपयोगकर्ताओं की गणना करने में असमर्थ।" + }, + { + "id": "api.license.add_license.array.app_error", + "translation": "अनुरोध में 'लाइसेंस' के तहत खाली सरणी।" + }, + { + "id": "api.ldap_group.not_found", + "translation": "एलडीएपी समूह नहीं मिला" + }, + { + "id": "api.file.get_file_preview.no_preview.app_error", + "translation": "फ़ाइल में पूर्वावलोकन छवि नहीं है।" + }, + { + "id": "api.context.local_origin_required.app_error", + "translation": "इस समापन बिंदु को स्थानीय अनुरोध मूल की आवश्यकता है।" + }, + { + "id": "api.command_shrug.name", + "translation": "श्रग्" + }, + { + "id": "api.command_share.uninvite_remote_id.help", + "translation": "आमंत्रण रद्द करने के लिए सुरक्षित कनेक्शन की आईडी।" + }, + { + "id": "api.command_share.remote_id.help", + "translation": "मौजूदा सुरक्षित कनेक्शन की आईडी। देखो `secure-connection` आदेश एक सुरक्षित कनेक्शन जोड़ने के लिए।" + }, + { + "id": "api.command_share.not_shared_channel_unshare", + "translation": "किसी चैनल को साझा नहीं किया जा सकता जो साझा नहीं किया गया है।" + }, + { + "id": "api.command_echo.name", + "translation": "एको" + }, + { + "id": "api.command_echo.high_volume.app_error", + "translation": "इको अनुरोध की उच्च मात्रा, अनुरोध को संसाधित नहीं कर सकती।" + }, + { + "id": "api.command_echo.desc", + "translation": "अपने खाते से एको बैक टेक्स्ट" + }, + { + "id": "api.command_custom_status.app_error", + "translation": "स्थिति सेट करने में त्रुटि." + }, + { + "id": "api.command_collapse.desc", + "translation": "छवि पूर्वावलोकन का स्वतः-संक्षिप्त करना चालू करें" + }, + { + "id": "api.templates.mfa_deactivated_body.title", + "translation": "बहु-कारक प्रमाणीकरण हटा दिया गया था" + }, + { + "id": "api.remote_cluster.save.app_error", + "translation": "सुरक्षित कनेक्शन सहेजने में हमें एक त्रुटि का सामना करना पड़ा।" + }, + { + "id": "api.post.send_notification_and_forget.push_comment_on_crt_thread", + "translation": " आप जिस धागे का अनुसरण कर रहे हैं उसका उत्तर दिया।" + }, + { + "id": "api.error_set_first_admin_visit_marketplace_status", + "translation": "स्टोर में पहले व्यवस्थापक विज़िट मार्केटप्लेस स्थिति को सहेजने का प्रयास करने में त्रुटि।" + }, + { + "id": "api.user.invalidate_verify_email_tokens_delete.error", + "translation": "ईमेल सत्यापन टोकन को अमान्य करते समय टोकन निकालने में असमर्थ" + }, + { + "id": "api.user.invalidate_verify_email_tokens.error", + "translation": "ईमेल सत्यापन टोकन को अमान्य करते समय प्रकार के अनुसार टोकन प्राप्त करने में असमर्थ" + }, + { + "id": "api.user.get_user_by_email.permissions.app_error", + "translation": "ईमेल द्वारा उपयोगकर्ता प्राप्त करने में असमर्थ।" + }, + { + "id": "api.user.get_uploads_for_user.forbidden.app_error", + "translation": "अपलोड प्राप्त करने में विफल।" + }, + { + "id": "api.user.email_to_oauth.not_available.app_error", + "translation": "प्रमाणीकरण स्थानांतरण कॉन्फ़िगर नहीं किया गया है या इस सर्वर पर उपलब्ध नहीं है।" + }, + { + "id": "api.user.email_to_ldap.not_available.app_error", + "translation": "इस सर्वर पर AD/LDAP उपलब्ध नहीं है।" + }, + { + "id": "api.user.demote_user_to_guest.already_guest.app_error", + "translation": "उपयोक्ता को अतिथि में बदलने में असमर्थ क्योंकि वह पहले से ही अतिथि है." + }, + { + "id": "api.user.delete_user.not_enabled.app_error", + "translation": "स्थायी उपयोगकर्ता हटाने की सुविधा सक्षम नहीं है। कृपया अपने सिस्टम व्यवस्थापक से संपर्क करें।" + }, + { + "id": "api.user.delete_team.not_enabled.app_error", + "translation": "स्थायी टीम हटाने की सुविधा सक्षम नहीं है। कृपया अपने सिस्टम व्यवस्थापक से संपर्क करें।" + }, + { + "id": "api.user.delete_channel.not_enabled.app_error", + "translation": "स्थायी चैनल हटाने की सुविधा सक्षम नहीं है। कृपया अपने सिस्टम व्यवस्थापक से संपर्क करें।" + }, + { + "id": "api.user.create_user.signup_link_invalid.app_error", + "translation": "साइनअप लिंक मान्य प्रतीत नहीं होता है।" + }, + { + "id": "api.user.create_user.signup_link_expired.app_error", + "translation": "साइनअप लिंक की समय सीमा समाप्त हो गई है।" + }, + { + "id": "api.user.create_user.signup_email_disabled.app_error", + "translation": "ईमेल के साथ उपयोगकर्ता साइन-अप अक्षम है।" + }, + { + "id": "api.user.create_user.no_open_server", + "translation": "यह सर्वर खुले साइनअप की अनुमति नहीं देता है। आमंत्रण प्राप्त करने के लिए कृपया अपने व्यवस्थापक से बात करें।" + }, + { + "id": "api.user.create_user.invalid_invitation_type.app_error", + "translation": "उपयोगकर्ता बनाने में असमर्थ, अमान्य आमंत्रण।" + }, + { + "id": "api.user.create_user.guest_accounts.license.app_error", + "translation": "आपका लाइसेंस अतिथि खातों का समर्थन नहीं करता है।" + }, + { + "id": "api.user.create_user.guest_accounts.disabled.app_error", + "translation": "अतिथि खाते अक्षम हैं।" + }, + { + "id": "api.user.create_user.disabled.app_error", + "translation": "उपयोगकर्ता निर्माण अक्षम है।" + }, + { + "id": "api.user.create_user.bad_token_email_data.app_error", + "translation": "टोकन में ईमेल पता उपयोगकर्ता डेटा से मेल नहीं खाता है।" + }, + { + "id": "api.user.create_user.accepted_domain.app_error", + "translation": "आपके द्वारा प्रदान किया गया ईमेल किसी स्वीकृत डोमेन से संबंधित नहीं है। कृपया अपने व्यवस्थापक से संपर्क करें या किसी भिन्न ईमेल से साइन अप करें।" + }, + { + "id": "api.user.create_profile_image.initial.app_error", + "translation": "उपयोगकर्ता के आरंभिक को डिफ़ॉल्ट प्रोफ़ाइल चित्र में नहीं जोड़ा जा सका।" + }, + { + "id": "api.user.create_profile_image.encode.app_error", + "translation": "डिफ़ॉल्ट प्रोफ़ाइल छवि को एन्कोड नहीं किया जा सका।" + }, + { + "id": "api.user.create_profile_image.default_font.app_error", + "translation": "डिफ़ॉल्ट प्रोफ़ाइल छवि फ़ॉन्ट नहीं बनाया जा सका." + }, + { + "id": "api.user.create_password_token.error", + "translation": "पासवर्ड पुनर्प्राप्ति टोकन बनाने में असमर्थ" + }, + { + "id": "api.user.create_oauth_user.create.app_error", + "translation": "{{.Service}} उपयोगकर्ता ऑब्जेक्ट से उपयोगकर्ता नहीं बनाया जा सका।" + }, + { + "id": "api.user.create_email_token.error", + "translation": "ईमेल सत्यापन के लिए टोकन डेटा बनाने में विफल" + }, + { + "id": "api.user.complete_switch_with_oauth.blank_email.app_error", + "translation": "खाली ईमेल।" + }, + { + "id": "api.user.check_user_password.invalid.app_error", + "translation": "अमान्य पासवर्ड के कारण लॉगिन विफल रहा।" + }, + { + "id": "api.user.check_user_mfa.bad_code.app_error", + "translation": "अमान्य एमएफए टोकन।" + }, + { + "id": "api.user.check_user_login_attempts.too_many.app_error", + "translation": "बहुत से विफल पासवर्ड प्रयासों के कारण आपका खाता लॉक कर दिया गया है। अपना पासवर्ड रीसेट करें।" + }, + { + "id": "api.user.autocomplete_users.missing_team_id.app_error", + "translation": "चैनल द्वारा स्वतः पूर्ण करने के लिए टीम आईडी पैरामीटर आवश्यक है।" + }, + { + "id": "api.user.authorize_oauth_user.token_failed.app_error", + "translation": "टोकन अनुरोध विफल रहा।" + }, + { + "id": "api.user.authorize_oauth_user.service.app_error", + "translation": "{{.Service}} को टोकन अनुरोध विफल रहा।" + }, + { + "id": "api.user.authorize_oauth_user.response.app_error", + "translation": "OAuth सेवा प्रदाता से अमान्य प्रतिक्रिया प्राप्त हुई।" + }, + { + "id": "api.user.authorize_oauth_user.missing.app_error", + "translation": "एक्सेस टोकन मौजूद नहीं है." + }, + { + "id": "api.user.authorize_oauth_user.invalid_state.app_error", + "translation": "अमान्य स्थिति" + }, + { + "id": "api.user.authorize_oauth_user.bad_token.app_error", + "translation": "खराब टोकन प्रकार।" + }, + { + "id": "api.user.authorize_oauth_user.bad_response.app_error", + "translation": "टोकन अनुरोध से खराब प्रतिक्रिया।" + }, + { + "id": "api.user.activate_mfa.email_and_ldap_only.app_error", + "translation": "इस प्रकार के खाते के लिए एमएफए उपलब्ध नहीं है।" + }, + { + "id": "api.upload.upload_data.invalid_content_type", + "translation": "मल्टीपार्ट अपलोड के लिए अमान्य सामग्री-प्रकार।" + }, + { + "id": "api.templates.invite_body.title.reminder", + "translation": "अनुस्मारक" + }, + { + "id": "api.templates.email_footer", + "translation": "अपनी अधिसूचना प्राथमिकताएं बदलने के लिए, अपनी टीम साइट पर लॉग इन करें और सेटिंग > सूचनाएं पर जाएं।" + }, + { + "id": "api.templates.cloud_welcome_email.signin_sub_info", + "translation": "कार्यक्षेत्र में साइन इन करें" + }, + { + "id": "api.team.update_team_scheme.scheme_scope.error", + "translation": "योजना को टीम में सेट करने में असमर्थ क्योंकि आपूर्ति की गई योजना टीम योजना नहीं है।" + }, + { + "id": "api.team.update_team_scheme.license.error", + "translation": "आपका लाइसेंस टीम की योजना को अपडेट करने का समर्थन नहीं करता" + }, + { + "id": "api.team.update_team_member_roles.guest_and_user.app_error", + "translation": "अमान्य टीम सदस्य अद्यतन: उपयोगकर्ता को अतिथि या उपयोगकर्ता होना चाहिए, लेकिन दोनों नहीं।" + }, + { + "id": "api.team.update_member_roles.not_a_member", + "translation": "निर्दिष्ट उपयोगकर्ता निर्दिष्ट टीम का सदस्य नहीं है।" + }, + { + "id": "api.team.team_icon.update.app_error", + "translation": "टीम आइकन को अपडेट करते समय एक त्रुटि हुई।" + }, + { + "id": "api.team.set_team_icon.write_file.app_error", + "translation": "टीम आइकन सहेजा नहीं जा सका।" + }, + { + "id": "api.team.set_team_icon.too_large.app_error", + "translation": "टीम आइकन अपलोड करने में असमर्थ. फ़ाइल बहुत बड़ी है।" + }, + { + "id": "api.team.set_team_icon.storage.app_error", + "translation": "टीम आइकन अपलोड करने में असमर्थ. छवि संग्रहण कॉन्फ़िगर नहीं किया गया है।" + }, + { + "id": "api.team.set_team_icon.parse.app_error", + "translation": "मल्टीपार्ट फॉर्म को पार्स नहीं किया जा सका।" + }, + { + "id": "api.team.set_team_icon.open.app_error", + "translation": "छवि फ़ाइल नहीं खोल सका।" + }, + { + "id": "api.team.set_team_icon.no_file.app_error", + "translation": "अनुरोध में 'छवि' के अंतर्गत कोई फ़ाइल नहीं है।" + }, + { + "id": "api.team.set_team_icon.get_team.app_error", + "translation": "टीम प्राप्त करने में एक त्रुटि हुई।" + }, + { + "id": "api.team.set_team_icon.encode.app_error", + "translation": "टीम आइकन को एन्कोड नहीं किया जा सका." + }, + { + "id": "api.team.set_team_icon.decode.app_error", + "translation": "टीम आइकन को डिकोड नहीं कर सका।" + }, + { + "id": "api.team.set_team_icon.check_image_limits.app_error", + "translation": "छवि सीमा जांच विफल। संकल्प बहुत अधिक है।" + }, + { + "id": "api.team.set_team_icon.array.app_error", + "translation": "अनुरोध में 'छवि' के तहत खाली सरणी।" + }, + { + "id": "api.team.move_channel.post.error", + "translation": "चैनल स्थानांतरण संदेश पोस्ट करने में विफल।" + }, + { + "id": "api.team.invite_guests_to_channels.invalid_body.app_error", + "translation": "अमान्य या अनुपलब्ध अनुरोध निकाय।" + }, + { + "id": "api.team.import_team.unknown_import_from.app_error", + "translation": "अज्ञात आयात स्रोत।" + }, + { + "id": "api.team.import_team.integer.app_error", + "translation": "Filesize एक पूर्णांक नहीं है।" + }, + { + "id": "api.team.import_team.array.app_error", + "translation": "अनुरोध में 'फ़ाइल' के तहत खाली सरणी।" + }, + { + "id": "api.team.add_team_member.invalid_body.app_error", + "translation": "अनुरोध के मुख्य भाग को पार्स करने में असमर्थ।" + }, + { + "id": "api.system.update_notices.validating_failed", + "translation": "उत्पाद सूचना शर्तों को सत्यापित करना विफल रहा" + }, + { + "id": "api.server.warn_metric.number_of_teams_5.notification_title", + "translation": "उन्नत अनुमतियों का उपयोग करना" + }, + { + "id": "api.server.warn_metric.email_us", + "translation": "हमें ईमेल करें" + }, + { + "id": "api.server.warn_metric.email_domain.notification_title", + "translation": "अतिथि खाते बनाना" + }, + { + "id": "api.server.warn_metric.email_domain.contact_us.email_body", + "translation": "Mattermost हमसे संपर्क करें अनुरोध। मुझे अतिथि खातों का उपयोग करने के बारे में और जानने में दिलचस्पी है।\n" + }, + { + "id": "api.server.warn_metric.bot_response.mailto_subject", + "translation": "Mattermost हमसे संपर्क करें अनुरोध" + }, + { + "id": "api.server.warn_metric.bot_response.mailto_site_url_header", + "translation": "साइट यूआरएल: {{.SiteUrl}}" + }, + { + "id": "api.server.warn_metric.bot_response.mailto_registered_users_header", + "translation": "कुल सक्रिय उपयोगकर्ता: {{.NoRegisteredUsers}}" + }, + { + "id": "api.server.warn_metric.bot_response.mailto_footer", + "translation": "यदि आपके पास कोई अतिरिक्त पूछताछ है, तो कृपया support@mattermost.com पर संपर्क करें" + }, + { + "id": "api.server.warn_metric.bot_response.mailto_email_header", + "translation": "ईमेल: {{.Email}}" + }, + { + "id": "api.server.warn_metric.bot_response.mailto_diagnostic_id_header", + "translation": "डायग्नोस्टिक आईडी: {{.DiagnosticId}}" + }, + { + "id": "api.server.warn_metric.bot_response.mailto_contact_header", + "translation": "संपर्क:{ {.Contact}}" + }, + { + "id": "api.server.start_server.starting.critical", + "translation": "सर्वर प्रारंभ करने में त्रुटि, err:%v" + }, + { + "id": "api.server.start_server.rate_limiting_rate_limiter", + "translation": "सीमित दर प्रारंभ करने में असमर्थ." + }, + { + "id": "api.server.start_server.rate_limiting_memory_store", + "translation": "दर सीमित स्मृति भंडार प्रारंभ करने में असमर्थ। मेमोरीस्टोरसाइज कॉन्फ़िगरेशन सेटिंग जांचें।" + }, + { + "id": "api.server.start_server.forward80to443.enabled_but_listening_on_wrong_port", + "translation": "पोर्ट %s पर सुनते समय पोर्ट 80 को पोर्ट 443 पर अग्रेषित करने में असमर्थ: यदि प्रॉक्सी सर्वर का उपयोग कर रहे हैं तो Forward80To443 अक्षम करें" + }, + { + "id": "api.server.start_server.forward80to443.disabled_while_using_lets_encrypt", + "translation": "LetsEncrypt का उपयोग करते समय Forward80To443 को सक्षम करना चाहिए" + }, + { + "id": "api.server.license_up_for_renewal.error_sending_email", + "translation": "नवीनीकरण ईमेल के लिए लाइसेंस भेजने में विफल" + }, + { + "id": "api.server.license_up_for_renewal.error_generating_link", + "translation": "लाइसेंस नवीनीकरण लिंक उत्पन्न करने में विफल" + }, + { + "id": "api.scheme.patch_scheme.license.error", + "translation": "आपका लाइसेंस अद्यतन अनुमति योजनाओं का समर्थन नहीं करता" + }, + { + "id": "api.scheme.get_teams_for_scheme.scope.error", + "translation": "योजना के लिए टीमें प्राप्त करने में असमर्थ क्योंकि आपूर्ति की गई योजना टीम योजना नहीं है।" + }, + { + "id": "api.scheme.get_channels_for_scheme.scope.error", + "translation": "योजना के लिए चैनल प्राप्त करने में असमर्थ क्योंकि आपूर्ति की गई योजना एक चैनल योजना नहीं है।" + }, + { + "id": "api.scheme.delete_scheme.license.error", + "translation": "आपका लाइसेंस हटाने की अनुमति योजनाओं का समर्थन नहीं करता" + }, + { + "id": "api.scheme.create_scheme.license.error", + "translation": "आपका लाइसेंस अनुमति योजना बनाने का समर्थन नहीं करता है।" + }, + { + "id": "api.roles.patch_roles.license.error", + "translation": "आपका लाइसेंस उन्नत अनुमतियों का समर्थन नहीं करता है।" + }, + { + "id": "api.restricted_system_admin", + "translation": "यह क्रिया प्रतिबंधित सिस्टम व्यवस्थापक के लिए प्रतिबंधित है।" + }, + { + "id": "api.remote_cluster.update_not_unique.app_error", + "translation": "उसी यूआरएल के साथ सुरक्षित कनेक्शन पहले से मौजूद है।" + }, + { + "id": "api.remote_cluster.update.app_error", + "translation": "सुरक्षित कनेक्शन को अपडेट करने में हमें एक त्रुटि का सामना करना पड़ा।" + }, + { + "id": "api.remote_cluster.service_not_enabled.app_error", + "translation": "दूरस्थ क्लस्टर सेवा सक्षम नहीं है।" + }, + { + "id": "api.remote_cluster.save_not_unique.app_error", + "translation": "सुरक्षित कनेक्शन पहले ही जोड़ा जा चुका है।" + }, + { + "id": "api.remote_cluster.invalid_topic.app_error", + "translation": "अमान्य विषय।" + }, + { + "id": "api.remote_cluster.invalid_id.app_error", + "translation": "अमान्य पहचान पत्र।" + }, + { + "id": "api.remote_cluster.get.app_error", + "translation": "सुरक्षित कनेक्शन को हटाने में हमें एक त्रुटि का सामना करना पड़ा।" + }, + { + "id": "api.remote_cluster.delete.app_error", + "translation": "सुरक्षित कनेक्शन को हटाने में हमें एक त्रुटि का सामना करना पड़ा।" + }, + { + "id": "api.reaction.save_reaction.user_id.app_error", + "translation": "आप अन्य उपयोगकर्ता के लिए प्रतिक्रिया सहेज नहीं सकते।" + }, + { + "id": "api.reaction.delete.archived_channel.app_error", + "translation": "आप किसी संग्रहीत चैनल में प्रतिक्रिया नहीं निकाल सकते।" + }, + { + "id": "api.push_notifications_ack.message.parse.app_error", + "translation": "पुश सूचना ack संदेश बनाने में कोई त्रुटि उत्पन्न हुई।" + }, + { + "id": "api.push_notifications_ack.forward.app_error", + "translation": "पुश सूचना सेवा को रसीद वितरण भेजते समय एक त्रुटि हुई।" + }, + { + "id": "api.preference.update_preferences.update_sidebar.app_error", + "translation": "अपडेट की गई प्राथमिकताओं से मेल खाने के लिए साइडबार को अपडेट करने में असमर्थ" + }, + { + "id": "api.preference.update_preferences.set.app_error", + "translation": "उपयोक्ता वरीयताएँ सेट करने में असमर्थ." + }, + { + "id": "api.preference.preferences_category.get.app_error", + "translation": "उपयोगकर्ता वरीयताएँ प्राप्त करने में असमर्थ।" + }, + { + "id": "api.preference.delete_preferences.update_sidebar.app_error", + "translation": "हटाई गई प्राथमिकताओं से मेल खाने के लिए साइडबार को अपडेट करने में असमर्थ" + }, + { + "id": "api.post.posts_by_ids.invalid_body.request_error", + "translation": "प्राप्त पोस्ट आईडी की संख्या अधिकतम आकार से अधिक हो गई है {{.MaxLength}}" + }, + { + "id": "api.post.error_get_post_id.pending", + "translation": "लंबित पद प्राप्त करने में असमर्थ।" + }, + { + "id": "api.post.do_action.action_integration.app_error", + "translation": "क्रिया एकीकरण त्रुटि।" + }, + { + "id": "api.post.deduplicate_create_post.failed_to_get", + "translation": "एक ही अनुरोध को दोहराने वाले क्लाइंट को डुप्लिकेट करने के बाद मूल पोस्ट लाने में विफल।" + }, + { + "id": "api.post.create_post.root_id.app_error", + "translation": "अमान्य RootId पैरामीटर।" + }, + { + "id": "api.post.create_post.channel_root_id.app_error", + "translation": "Invalid ChannelId के लिये RootId पैरामीटर।" + }, + { + "id": "api.post.create_post.can_not_post_to_deleted.error", + "translation": "हटाए गए चैनल पर पोस्ट नहीं कर सकते।" + }, + { + "id": "api.post.check_for_out_of_channel_mentions.message.one", + "translation": "@{{.Username}} इस उल्लेख द्वारा अधिसूचित नहीं किया गया क्योंकि वे चैनल में नहीं हैं।" + }, + { + "id": "api.post.check_for_out_of_channel_mentions.message.multiple", + "translation": "@{{.Usernames}} और @{{.LastUsername}} इस उल्लेख द्वारा अधिसूचित नहीं किया गया क्योंकि वे चैनल में नहीं हैं।" + }, + { + "id": "api.post.check_for_out_of_channel_groups_mentions.message.one", + "translation": "@{{.Username}} इस उल्लेख द्वारा अधिसूचित नहीं किया गया क्योंकि वे चैनल में नहीं हैं। उन्हें चैनल में नहीं जोड़ा जा सकता क्योंकि वे लिंक किए गए समूहों के सदस्य नहीं हैं। उन्हें इस चैनल में जोड़ने के लिए, उन्हें लिंक किए गए समूहों में जोड़ा जाना चाहिए।" + }, + { + "id": "api.post.check_for_out_of_channel_groups_mentions.message.multiple", + "translation": "@{{.Usernames}} तथा @{{.LastUsername}} इस उल्लेख द्वारा अधिसूचित नहीं किया गया क्योंकि वे चैनल में नहीं हैं। उन्हें चैनल में नहीं जोड़ा जा सकता क्योंकि वे लिंक किए गए समूहों के सदस्य नहीं हैं। उन्हें इस चैनल में जोड़ने के लिए, उन्हें लिंक किए गए समूहों में जोड़ा जाना चाहिए।" + }, + { + "id": "api.plugin.upload.file.app_error", + "translation": "मल्टीपार्ट/फॉर्म अनुरोध में फ़ाइल खोलने में असमर्थ।" + }, + { + "id": "api.plugin.upload.array.app_error", + "translation": "मल्टीपार्ट/फॉर्म अनुरोध में फ़ाइल सरणी खाली है।" + }, + { + "id": "api.oauth.revoke_access_token.get.app_error", + "translation": "हटाने से पहले डीबी से एक्सेस टोकन प्राप्त करने में त्रुटि।" + }, + { + "id": "api.oauth.get_access_token.missing_code.app_error", + "translation": "अमान्य_अनुरोध: गुम कोड।" + }, + { + "id": "api.oauth.get_access_token.bad_grant.app_error", + "translation": "अमान्य_अनुरोध: गलत अनुदान_प्रकार।" + }, + { + "id": "api.oauth.get_access_token.bad_client_secret.app_error", + "translation": "अमान्य_अनुरोध: अनुपलब्ध client_secret." + }, + { + "id": "api.oauth.get_access_token.bad_client_id.app_error", + "translation": "अमान्य_अनुरोध: खराब क्लाइंट_आईडी।" + }, + { + "id": "api.oauth.allow_oauth.redirect_callback.app_error", + "translation": "अमान्य_अनुरोध: आपूर्ति की गई रीडायरेक्ट_यूरी पंजीकृत कॉलबैक_यूआरएल से मेल नहीं खाती।" + }, + { + "id": "api.job.unable_to_create_job.incorrect_job_type", + "translation": "आप जिस कार्य को बनाने का प्रयास कर रहे हैं वह कार्य प्रकार अमान्य है" + }, + { + "id": "api.job.retrieve.nopermissions", + "translation": "आप जिस प्रकार की नौकरी को पुनः प्राप्त करने का प्रयास कर रहे हैं, उसमें अनुमतियाँ नहीं हैं" + }, + { + "id": "api.invalid_channel", + "translation": "अनुरोध में सूचीबद्ध चैनल उपयोगकर्ता का नहीं है" + }, + { + "id": "api.file.read_file.reading_local.app_error", + "translation": "स्थानीय सर्वर फ़ाइल संग्रहण से पढ़ने में त्रुटि आई।" + }, + { + "id": "api.custom_status.set_custom_statuses.update.app_error", + "translation": "पसंद के मुताबिक स्थिति अपडेट करने में विफल. कृपया इमोजी या कस्टम टेक्स्ट स्थिति या दोनों जोड़ें।" + }, + { + "id": "api.command_share.uninvite_remote.help", + "translation": "इस साझा चैनल से एक सुरक्षित कनेक्शन को आमंत्रित करता है" + }, + { + "id": "api.command_share.invitation_sent", + "translation": "साझा चैनल आमंत्रण भेज दिया गया है `{{.Name}} {{.SiteURL}}`." + }, + { + "id": "app.import.validate_reaction_import_data.emoji_name_length.error", + "translation": "प्रतिक्रिया इमोजीनाम संपत्ति अधिकतम अनुमत लंबाई से अधिक लंबी है।" + }, + { + "id": "app.import.validate_reaction_import_data.create_at_zero.error", + "translation": "प्रतिक्रिया CreateAt गुण शून्य नहीं होना चाहिए।" + }, + { + "id": "app.import.validate_reaction_import_data.create_at_missing.error", + "translation": "आवश्यक प्रतिक्रिया गुण गुम है: create_at." + }, + { + "id": "app.import.validate_reaction_import_data.create_at_before_parent.error", + "translation": "प्रतिक्रिया CreateAt संपत्ति मूल पोस्ट CreateAt से बड़ी होनी चाहिए।" + }, + { + "id": "app.import.validate_post_import_data.team_missing.error", + "translation": "आवश्यक पोस्ट संपत्ति गुम है: Team." + }, + { + "id": "app.import.validate_post_import_data.user_missing.error", + "translation": "आवश्यक पोस्ट संपत्ति गुम है: User." + }, + { + "id": "app.import.validate_post_import_data.props_too_large.error", + "translation": "पोस्ट प्रॉप्स अधिकतम अनुमत लंबाई से अधिक लंबे होते हैं।" + }, + { + "id": "app.import.validate_emoji_import_data.name_missing.error", + "translation": "आयात इमोजी नाम फ़ील्ड अनुपलब्ध या रिक्त है।" + }, + { + "id": "app.import.validate_emoji_import_data.image_missing.error", + "translation": "आयात इमोजी छवि फ़ील्ड अनुपलब्ध या रिक्त है।" + }, + { + "id": "app.import.process_import_data_file_version_line.invalid_version.error", + "translation": "डेटा आयात फ़ाइल का संस्करण पढ़ने में असमर्थ।" + }, + { + "id": "app.import.import_user_teams.save_preferences.error", + "translation": "टीम थीम प्राथमिकताएं सहेजने में असमर्थ" + }, + { + "id": "app.import.import_user_teams.save_members.max_accounts.app_error", + "translation": "टीम सदस्यता आयात करने में असमर्थ क्योंकि उस टीम में अधिक सदस्यों की अनुमति नहीं है" + }, + { + "id": "app.import.import_user_teams.save_members.error", + "translation": "टीम सदस्यता आयात करने में असमर्थ" + }, + { + "id": "app.import.import_user_teams.save_members.conflict.app_error", + "translation": "नई टीम सदस्यता आयात करने में असमर्थ क्योंकि यह पहले से मौजूद है" + }, + { + "id": "app.import.import_user_channels.save_preferences.error", + "translation": "उपयोगकर्ता चैनल सदस्यता आयात करने में त्रुटि। प्राथमिकताएं सहेजने में विफल।" + }, + { + "id": "app.import.import_user_channels.channel_not_found.error", + "translation": "उपयोगकर्ता चैनल आयात करने में त्रुटि। चैनल नहीं मिला।" + }, + { + "id": "app.import.import_user.save_preferences.error", + "translation": "उपयोगकर्ता प्राथमिकताएं आयात करने में त्रुटि. प्राथमिकताएं सहेजने में विफल।" + }, + { + "id": "app.import.import_team.scheme_wrong_scope.error", + "translation": "टीम को टीम के दायरे वाली योजना के लिए असाइन किया जाना चाहिए।" + }, + { + "id": "app.import.import_team.scheme_deleted.error", + "translation": "किसी टीम को हटाई गई योजना का उपयोग करने के लिए सेट करने में असमर्थ।" + }, + { + "id": "app.import.import_scheme.scope_change.error", + "translation": "थोक आयातक पहले से मौजूद योजना के दायरे को नहीं बदल सकता है।" + }, + { + "id": "app.import.import_post.user_not_found.error", + "translation": "पोस्ट आयात करने में त्रुटि. उपयोगकर्ता नाम \"{{.Username}}\" वाला उपयोगकर्ता नहीं मिला।" + }, + { + "id": "app.import.import_post.save_preferences.error", + "translation": "पोस्ट आयात करने में त्रुटि. प्राथमिकताएं सहेजने में विफल।" + }, + { + "id": "app.import.import_line.null_user.error", + "translation": "आयात डेटा लाइन में \"उपयोगकर्ता\" प्रकार है लेकिन उपयोगकर्ता ऑब्जेक्ट शून्य है।" + }, + { + "id": "app.import.import_line.null_team.error", + "translation": "आयात डेटा लाइन में \"team\" टाइप है लेकिन टीम ऑब्जेक्ट शून्य है।" + }, + { + "id": "app.import.import_line.null_scheme.error", + "translation": "आयात डेटा लाइन में \"स्कीम\" टाइप है लेकिन स्कीम ऑब्जेक्ट शून्य है।" + }, + { + "id": "app.import.import_line.null_post.error", + "translation": "आयात डेटा लाइन में \"पोस्ट\" टाइप है लेकिन पोस्ट ऑब्जेक्ट शून्य है।" + }, + { + "id": "app.import.import_line.null_emoji.error", + "translation": "आयात डेटा लाइन में \"emoji\" टाइप है लेकिन इमोजी ऑब्जेक्ट शून्य है।" + }, + { + "id": "app.import.import_line.null_direct_post.error", + "translation": "आयात डेटा लाइन में \"direct_post\" टाइप है लेकिन Direct_post ऑब्जेक्ट शून्य है।" + }, + { + "id": "app.import.import_line.null_direct_channel.error", + "translation": "आयात डेटा लाइन में \"direct_channel\" टाइप है लेकिन Direct_channel ऑब्जेक्ट शून्य है।" + }, + { + "id": "app.import.import_line.null_channel.error", + "translation": "आयात डेटा लाइन में \"चैनल\" टाइप है लेकिन चैनल ऑब्जेक्ट शून्य है।" + }, + { + "id": "app.import.import_direct_post.create_group_channel.error", + "translation": "समूह चैनल प्राप्त करने में विफल" + }, + { + "id": "app.import.import_direct_post.create_direct_channel.error", + "translation": "सीधा चैनल पाने में विफल" + }, + { + "id": "app.import.import_direct_channel.update_header_failed.error", + "translation": "सीधा चैनल हैडर अपडेट करने में विफल" + }, + { + "id": "app.import.import_direct_channel.create_group_channel.error", + "translation": "समूह चैनल बनाने में विफल" + }, + { + "id": "app.import.import_direct_channel.create_direct_channel.error", + "translation": "सीधा चैनल बनाने में विफल" + }, + { + "id": "app.import.import_channel.scheme_wrong_scope.error", + "translation": "चैनल को चैनल-स्कोप्ड स्कीम को असाइन किया जाना चाहिए।" + }, + { + "id": "app.import.import_channel.scheme_deleted.error", + "translation": "किसी चैनल को हटाई गई योजना का उपयोग करने के लिए सेट करने में असमर्थ।" + }, + { + "id": "app.import.emoji.bad_file.error", + "translation": "आयात इमोजी छवि फ़ाइल पढ़ने में त्रुटि। नाम वाला इमोजी: \"{{.EmojiName}}\"" + }, + { + "id": "app.import.bulk_import.unsupported_version.error", + "translation": "डेटा आयात फ़ाइल में गलत या अनुपलब्ध संस्करण। सुनिश्चित करें कि संस्करण आपकी आयात फ़ाइल में पहला ऑब्जेक्ट है और पुनः प्रयास करें।" + }, + { + "id": "app.import.bulk_import.file_scan.error", + "translation": "आयात डेटा फ़ाइल पढ़ने में त्रुटि।" + }, + { + "id": "app.file_info.permanent_delete_by_user.app_error", + "translation": "प्रयोक्ता के अनुलग्‍नक हटाने में असमर्थ।" + }, + { + "id": "app.export.export_write_line.json_marshall.error", + "translation": "निर्यात के लिए JSON डेटा को मार्शल करते हुए एक त्रुटि हुई।" + }, + { + "id": "app.export.export_write_line.io_writer.error", + "translation": "निर्यात डेटा लिखते समय एक त्रुटि हुई।" + }, + { + "id": "app.email.setup_rate_limiter.app_error", + "translation": "दर सीमक में त्रुटि हुई।" + }, + { + "id": "app.email.no_rate_limiter.app_error", + "translation": "दर सीमक स्थापित नहीं है।" + }, + { + "id": "app.command.updatecommand.internal_error", + "translation": "आदेश को अद्यतन करने में असमर्थ।" + }, + { + "id": "app.command.tryexecutecustomcommand.internal_error", + "translation": "कस्टम आदेश निष्पादित करने में असमर्थ।" + }, + { + "id": "app.command.regencommandtoken.internal_error", + "translation": "आदेश टोकन पुन: उत्पन्न करने में असमर्थ।" + }, + { + "id": "app.command.movecommand.internal_error", + "translation": "आदेश को स्थानांतरित करने में असमर्थ।" + }, + { + "id": "app.command.listteamcommands.internal_error", + "translation": "टीम कमांड को सूचीबद्ध करने में असमर्थ।" + }, + { + "id": "app.command.listautocompletecommands.internal_error", + "translation": "स्वत: पूर्ण आदेशों को सूचीबद्ध करने में असमर्थ।" + }, + { + "id": "app.command.listallcommands.internal_error", + "translation": "आदेशों को सूचीबद्ध करने में असमर्थ।" + }, + { + "id": "app.command.getcommand.internal_error", + "translation": "आदेश प्राप्त करने में असमर्थ।" + }, + { + "id": "app.command.deletecommand.internal_error", + "translation": "आदेश को हटाने में असमर्थ।" + }, + { + "id": "app.command.createcommand.internal_error", + "translation": "आदेश सहेजने में असमर्थ।" + }, + { + "id": "app.channel_member_history.log_leave_event.internal_error", + "translation": "चैनल सदस्य इतिहास रिकॉर्ड करने में विफल। मौजूदा शामिल होने के रिकॉर्ड को अपडेट करने में विफल" + }, + { + "id": "app.channel_member_history.log_join_event.internal_error", + "translation": "चैनल सदस्य इतिहास रिकॉर्ड करने में विफल।" + }, + { + "id": "app.channel.user_belongs_to_channels.app_error", + "translation": "यह निर्धारित करने में असमर्थ कि उपयोगकर्ता चैनलों की सूची से संबंधित है या नहीं।" + }, + { + "id": "app.channel.update_last_viewed_at_post.app_error", + "translation": "चैनल को अपठित के रूप में चिह्नित करने में असमर्थ।" + }, + { + "id": "app.channel.update_last_viewed_at.app_error", + "translation": "अंतिम बार देखे गए समय को अपडेट करने में असमर्थ।" + }, + { + "id": "app.channel.update_channel.internal_error", + "translation": "चैनल अपडेट करने में असमर्थ।" + }, + { + "id": "app.channel.update.bad_id", + "translation": "चैनल अपडेट करने में असमर्थ।" + }, + { + "id": "app.channel.sidebar_categories.app_error", + "translation": "डेटाबेस में रिकॉर्ड डालने में विफल।" + }, + { + "id": "app.channel.search_group_channels.app_error", + "translation": "दिए गए उपयोगकर्ता और अवधि के लिए समूह चैनल प्राप्त करने में असमर्थ।" + }, + { + "id": "app.channel.search.app_error", + "translation": "हमें चैनल खोजने में त्रुटि का सामना करना पड़ा।" + }, + { + "id": "app.channel.save_member.exists.app_error", + "translation": "उस आईडी वाला एक चैनल सदस्य पहले से मौजूद है।" + }, + { + "id": "app.channel.restore.app_error", + "translation": "चैनल को पुनर्स्थापित करने में असमर्थ।" + }, + { + "id": "app.channel.reset_all_channel_schemes.app_error", + "translation": "हम चैनल योजनाओं को रीसेट नहीं कर सके।" + }, + { + "id": "app.channel.remove_member.app_error", + "translation": "चैनल सदस्य को निकालने में असमर्थ।" + }, + { + "id": "app.channel.remove_all_deactivated_members.app_error", + "translation": "हम निष्क्रिय किए गए उपयोगकर्ताओं को चैनल से नहीं निकाल सके।" + }, + { + "id": "app.channel.post_update_channel_purpose_message.updated_to", + "translation": "%s ने चैनल के उद्देश्य को इस पर अपडेट किया: %s" + }, + { + "id": "app.channel.post_update_channel_purpose_message.removed", + "translation": "%s ने चैनल का उद्देश्य हटा दिया (was: %s)" + }, + { + "id": "app.channel.post_update_channel_purpose_message.post.error", + "translation": "चैनल उद्देश्य संदेश पोस्ट करने में विफल" + }, + { + "id": "app.channel.pinned_posts.app_error", + "translation": "पिन की गई पोस्ट ढूंढने में असमर्थ।" + }, + { + "id": "app.channel.permanent_delete_members_by_user.app_error", + "translation": "चैनल सदस्य को निकालने में असमर्थ।" + }, + { + "id": "app.channel.permanent_delete.app_error", + "translation": "चैनल को हटाने में असमर्थ।" + }, + { + "id": "app.channel.move_channel.members_do_not_match.error", + "translation": "किसी चैनल को तब तक स्थानांतरित करने में असमर्थ जब तक कि उसके सभी सदस्य पहले से ही गंतव्य टीम के सदस्य न हों।" + }, + { + "id": "app.channel.migrate_channel_members.select.app_error", + "translation": "चैनल सदस्यों के बैच का चयन करने में विफल।" + }, + { + "id": "app.channel.get_unread.app_error", + "translation": "चैनल को अपठित संदेश प्राप्त करने में असमर्थ।" + }, + { + "id": "app.channel.get_public_channels.get.app_error", + "translation": "सार्वजनिक चैनल प्राप्त करने में असमर्थ।" + }, + { + "id": "app.channel.get_private_channels.get.app_error", + "translation": "निजी चैनल प्राप्त करने में असमर्थ।" + }, + { + "id": "app.channel.get_pinnedpost_count.app_error", + "translation": "चैनल पिन की गई पोस्ट गणना प्राप्त करने में असमर्थ।" + }, + { + "id": "app.channel.get_more_channels.get.app_error", + "translation": "चैनल प्राप्त करने में असमर्थ।" + }, + { + "id": "app.channel.get_members_by_ids.app_error", + "translation": "चैनल के सदस्य प्राप्त करने में असमर्थ।" + }, + { + "id": "app.channel.get_members.app_error", + "translation": "चैनल के सदस्य प्राप्त करने में असमर्थ।" + }, + { + "id": "app.channel.get_member_count.app_error", + "translation": "चैनल सदस्य संख्या प्राप्त करने में असमर्थ।" + }, + { + "id": "app.channel.get_member.missing.app_error", + "translation": "उस उपयोगकर्ता आईडी और चैनल आईडी के लिए कोई चैनल सदस्य नहीं मिला।" + }, + { + "id": "app.channel.get_member.app_error", + "translation": "चैनल सदस्य प्राप्त करने में असमर्थ।" + }, + { + "id": "app.channel.get_for_post.app_error", + "translation": "दी गई पोस्ट के लिए चैनल प्राप्त करने में असमर्थ।" + }, + { + "id": "app.channel.get_deleted.missing.app_error", + "translation": "कोई हटाए गए चैनल मौजूद नहीं हैं।" + }, + { + "id": "app.channel.get_deleted.existing.app_error", + "translation": "मौजूदा हटाए गए चैनल को खोजने में असमर्थ।" + }, + { + "id": "app.channel.get_channels_by_ids.not_found.app_error", + "translation": "कोई चैनल नहीं मिला।" + }, + { + "id": "app.channel.get_channels_by_ids.get.app_error", + "translation": "चैनल प्राप्त करने में असमर्थ।" + }, + { + "id": "app.channel.get_channels_by_ids.app_error", + "translation": "आईडी द्वारा चैनल प्राप्त करने में असमर्थ।" + }, + { + "id": "app.channel.get_channels_batch_for_indexing.get.app_error", + "translation": "अनुक्रमण के लिए चैनल बैच प्राप्त करने में असमर्थ।" + }, + { + "id": "app.channel.get_channels.not_found.app_error", + "translation": "कोई चैनल नहीं मिला।" + }, + { + "id": "app.channel.get_channels.get.app_error", + "translation": "चैनल प्राप्त करने में असमर्थ।" + }, + { + "id": "app.channel.get_channel_counts.get.app_error", + "translation": "चैनल की गिनती प्राप्त करने में असमर्थ।" + }, + { + "id": "app.channel.get_by_scheme.app_error", + "translation": "प्रदान की गई योजना के लिए चैनल प्राप्त करने में असमर्थ।" + }, + { + "id": "app.channel.get_by_name.missing.app_error", + "translation": "चैनल मौजूद नहीं है।" + }, + { + "id": "app.channel.get_by_name.existing.app_error", + "translation": "मौजूदा चैनल खोजने में असमर्थ।" + }, + { + "id": "app.channel.get_all_direct.app_error", + "translation": "सभी प्रत्यक्ष चैनल प्राप्त करने में असमर्थ।" + }, + { + "id": "app.channel.get_all_channels_count.app_error", + "translation": "सभी चैनल गिनने में असमर्थ।" + }, + { + "id": "app.channel.get_all_channels.app_error", + "translation": "सभी चैनल प्राप्त करने में असमर्थ।" + }, + { + "id": "app.channel.get_all.app_error", + "translation": "सभी चैनल प्राप्त करने में असमर्थ।" + }, + { + "id": "app.channel.get.find.app_error", + "translation": "हमें चैनल खोजने में एक त्रुटि का सामना करना पड़ा।" + }, + { + "id": "app.channel.get.existing.app_error", + "translation": "मौजूदा चैनल खोजने में असमर्थ।" + }, + { + "id": "app.channel.delete.app_error", + "translation": "चैनल को हटाने में असमर्थ।" + }, + { + "id": "app.channel.create_initial_sidebar_categories.internal_error", + "translation": "उपयोगकर्ता के लिए आरंभिक साइडबार श्रेणियां बनाने में असमर्थ।" + }, + { + "id": "app.channel.create_direct_channel.internal_error", + "translation": "सीधा चैनल सहेजने में असमर्थ।" + }, + { + "id": "app.audit.save.saving.app_error", + "translation": "हमें लेखापरीक्षा को सहेजने में एक त्रुटि का सामना करना पड़ा।" + }, + { + "id": "app.admin.saml.invalid_response_from_idp.app_error", + "translation": "पहचान प्रदाता से प्राप्त प्रतिक्रिया को नहीं पढ़ सका।" + }, + { + "id": "api.user.view_archived_channels.get_users_in_channel.app_error", + "translation": "किसी संग्रहीत चैनल के लिए उपयोगकर्ताओं को पुनर्प्राप्त नहीं किया जा सकता" + }, + { + "id": "api.user.view_archived_channels.get_posts_for_channel.app_error", + "translation": "किसी संग्रहीत चैनल के लिए पोस्ट पुनर्प्राप्त नहीं कर सकता" + }, + { + "id": "api.user.upload_profile_user.array.app_error", + "translation": "अनुरोध में 'छवि' के तहत खाली सरणी।" + }, + { + "id": "api.user.update_user_roles.license.app_error", + "translation": "कस्टम अनुमति योजनाएं वर्तमान लाइसेंस द्वारा समर्थित नहीं हैं" + }, + { + "id": "api.user.update_active.cannot_enable_guest_when_guest_feature_is_disabled.app_error", + "translation": "आप अतिथि खाते को सक्रिय नहीं कर सकते क्योंकि अतिथि पहुंच सुविधा सक्षम नहीं है।" + }, + { + "id": "api.user.send_verify_email_and_forget.failed.error", + "translation": "सत्यापन ईमेल सफलतापूर्वक भेजने में विफल" + }, + { + "id": "api.user.send_sign_in_change_email_and_forget.error", + "translation": "अपडेट पासवर्ड ईमेल सफलतापूर्वक भेजने में विफल" + }, + { + "id": "api.user.send_email_change_verify_email_and_forget.error", + "translation": "ईमेल परिवर्तन सत्यापन ईमेल सफलतापूर्वक भेजने में विफल" + }, + { + "id": "api.user.send_deactivate_email_and_forget.failed.error", + "translation": "निष्क्रिय खाता ईमेल सफलतापूर्वक भेजने में विफल" + }, + { + "id": "api.user.login.guest_accounts.disabled.error", + "translation": "अतिथि खाते अक्षम हैं" + }, + { + "id": "api.user.login.client_side_cert.license.app_error", + "translation": "एक वैध एंटरप्राइज़ लाइसेंस के बिना प्रायोगिक सुविधा ClientSideCertEnable का उपयोग करने का प्रयास करें।" + }, + { + "id": "api.user.login.client_side_cert.certificate.app_error", + "translation": "एक मान्य प्रमाणपत्र प्रदान किए बिना प्रयोगात्मक सुविधा ClientSideCert का उपयोग करके साइन इन करने का प्रयास किया गया।" + }, + { + "id": "api.user.login.bot_login_forbidden.app_error", + "translation": "बॉट लॉगिन निषिद्ध है।" + }, + { + "id": "api.user.login.blank_pwd.app_error", + "translation": "पासवर्ड फ़ील्ड खाली नहीं होना चाहिए" + }, + { + "id": "api.user.ldap_to_email.not_ldap_account.app_error", + "translation": "यह उपयोगकर्ता खाता AD/LDAP का उपयोग नहीं करता है।" + }, + { + "id": "api.user.ldap_to_email.not_available.app_error", + "translation": "इस सर्वर पर AD/LDAP उपलब्ध नहीं है।" + }, + { + "id": "api.user.invalidate_verify_email_tokens_parse.error", + "translation": "ईमेल सत्यापन टोकन को अमान्य करते समय टोकन निकालने में असमर्थ" + }, + { + "id": "app.import.validate_user_import_data.advanced_props_show_unread_section.error", + "translation": "उपयोगकर्ता के लिए अमान्य शो अपठित अनुभाग सेटिंग" + }, + { + "id": "app.import.validate_user_import_data.advanced_props_formatting.error", + "translation": "उपयोगकर्ता के लिए अमान्य पोस्ट स्वरूपण सेटिंग" + }, + { + "id": "app.import.validate_user_import_data.advanced_props_feature_markdown_preview.error", + "translation": "उपयोगकर्ता के लिए अमान्य मार्कडाउन पूर्वावलोकन सेटिंग" + }, + { + "id": "app.import.validate_user_import_data.advanced_props_email_interval.error", + "translation": "उपयोगकर्ता के लिए अमान्य ईमेल बैचिंग अंतराल सेटिंग" + }, + { + "id": "app.import.validate_user_channels_import_data.invalid_roles.error", + "translation": "उपयोगकर्ता की चैनल सदस्यता के लिए अमान्य भूमिकाएं." + }, + { + "id": "app.import.validate_user_channels_import_data.invalid_notify_props_mobile.error", + "translation": "उपयोगकर्ता की चैनल सदस्यता के लिए अमान्य मोबाइल NotifyProps।" + }, + { + "id": "app.import.validate_user_channels_import_data.invalid_notify_props_mark_unread.error", + "translation": "उपयोगकर्ता की चैनल सदस्यता के लिए अमान्य MarkUnread NotifyProps।" + }, + { + "id": "app.import.validate_user_channels_import_data.invalid_notify_props_desktop.error", + "translation": "उपयोगकर्ता की चैनल सदस्यता के लिए अमान्य डेस्कटॉप NotifyProps।" + }, + { + "id": "app.import.validate_user_channels_import_data.channel_name_missing.error", + "translation": "उपयोगकर्ता की चैनल सदस्यता से चैनल का नाम गायब है।" + }, + { + "id": "app.import.validate_team_import_data.type_missing.error", + "translation": "आवश्यक टीम संपत्ति गुम है: प्रकार।" + }, + { + "id": "app.import.validate_team_import_data.type_invalid.error", + "translation": "टीम प्रकार मान्य नहीं है।" + }, + { + "id": "app.import.validate_team_import_data.scheme_invalid.error", + "translation": "टीम के लिए अमान्य योजना का नाम।" + }, + { + "id": "app.import.validate_team_import_data.name_reserved.error", + "translation": "टीम के नाम में आरक्षित शब्द हैं।" + }, + { + "id": "app.import.validate_team_import_data.name_missing.error", + "translation": "टीम की आवश्यक संपत्ति गुम है: name." + }, + { + "id": "app.import.validate_team_import_data.name_length.error", + "translation": "टीम का नाम बहुत लंबा है।" + }, + { + "id": "app.import.validate_team_import_data.name_characters.error", + "translation": "टीम के नाम में अमान्य वर्ण हैं।" + }, + { + "id": "app.import.validate_team_import_data.display_name_missing.error", + "translation": "टीम की आवश्यक संपत्ति गुम है: display_name." + }, + { + "id": "app.import.validate_team_import_data.display_name_length.error", + "translation": "टीम display_name अनुमत लंबाई सीमा के भीतर नहीं है।" + }, + { + "id": "app.import.validate_team_import_data.description_length.error", + "translation": "टीम विवरण बहुत लंबा है।" + }, + { + "id": "app.import.validate_scheme_import_data.wrong_roles_for_scope.error", + "translation": "इस दायरे वाली योजना के लिए गलत भूमिकाएँ प्रदान की गईं।" + }, + { + "id": "app.import.validate_scheme_import_data.unknown_scheme.error", + "translation": "अज्ञात योजना का दायरा।" + }, + { + "id": "app.import.validate_scheme_import_data.null_scope.error", + "translation": "योजना का दायरा आवश्यक है।" + }, + { + "id": "app.import.validate_scheme_import_data.name_invalid.error", + "translation": "अमान्य योजना का नाम।" + }, + { + "id": "app.import.validate_scheme_import_data.display_name_invalid.error", + "translation": "अमान्य योजना प्रदर्शन नाम।" + }, + { + "id": "app.import.validate_scheme_import_data.description_invalid.error", + "translation": "अमान्य योजना विवरण।" + }, + { + "id": "app.import.validate_role_import_data.name_invalid.error", + "translation": "अमान्य भूमिका का नाम।" + }, + { + "id": "app.import.validate_role_import_data.invalid_permission.error", + "translation": "भूमिका पर अमान्य अनुमति।" + }, + { + "id": "app.import.validate_role_import_data.display_name_invalid.error", + "translation": "अमान्य भूमिका प्रदर्शन नाम." + }, + { + "id": "app.import.validate_role_import_data.description_invalid.error", + "translation": "अमान्य भूमिका विवरण." + }, + { + "id": "app.import.validate_reply_import_data.user_missing.error", + "translation": "आवश्यक उत्तर गुण गुम है: User." + }, + { + "id": "app.import.validate_reply_import_data.message_missing.error", + "translation": "गुम आवश्यक उत्तर संपत्ति: Message." + }, + { + "id": "app.import.validate_reply_import_data.message_length.error", + "translation": "उत्तर संदेश गुण अधिकतम अनुमत लंबाई से अधिक लंबा है।" + }, + { + "id": "app.import.validate_reply_import_data.create_at_zero.error", + "translation": "उत्तर दें CreateAt गुण शून्य नहीं होना चाहिए।" + }, + { + "id": "app.import.validate_reply_import_data.create_at_missing.error", + "translation": "आवश्यक उत्तर गुण गुम है: create_at." + }, + { + "id": "app.import.validate_reaction_import_data.user_missing.error", + "translation": "आवश्यक प्रतिक्रिया गुण गुम है: User." + }, + { + "id": "app.import.validate_reaction_import_data.emoji_name_missing.error", + "translation": "आवश्यक प्रतिक्रिया गुण गुम है: इमोजीनाम।" + }, + { + "id": "app.import.import_post.channel_not_found.error", + "translation": "पोस्ट आयात करने में त्रुटि. \"{{.ChannelName}}\" नाम वाला चैनल नहीं मिला।" + }, + { + "id": "app.import.import_line.unknown_line_type.error", + "translation": "आयात डेटा लाइन में अज्ञात प्रकार \"{{.Type}}\" है।" + }, + { + "id": "app.channel.post_update_channel_purpose_message.updated_from", + "translation": "%s ने चैनल के उद्देश्य को इस से अपडेट किया: %s से: %s" + }, + { + "id": "app.channel.post_update_channel_purpose_message.retrieve_user.error", + "translation": "चैनल उद्देश्य संदेश %v . को अपडेट करते समय उपयोगकर्ता को पुनः प्राप्त करने में विफल" + }, + { + "id": "api.user.create_oauth_user.already_attached.app_error", + "translation": "{{.Service}} के अलावा किसी अन्य साइन इन पद्धति का उपयोग करके उस ईमेल पते से पहले से एक खाता संबद्ध है। कृपया {{.Auth}} का उपयोग करके साइन इन करें।" + }, + { + "id": "api.user.complete_switch_with_oauth.parse.app_error", + "translation": "{{.Service}} उपयोगकर्ता ऑब्जेक्ट से प्रमाणीकरण डेटा पार्स नहीं किया जा सका।" + }, + { + "id": "api.user.authorize_oauth_user.unsupported.app_error", + "translation": "{{.Service}} OAuth 2.0 के माध्यम से SSO इस सर्वर पर उपलब्ध नहीं है।" + }, + { + "id": "api.templates.mfa_deactivated_body.info", + "translation": "{{ .SiteURL }} पर आपके खाते से बहु-कारक प्रमाणीकरण हटा दिया गया है।" + }, + { + "id": "api.team.import_team.unavailable.app_error", + "translation": "विकृत अनुरोध: फाइलसाइज फील्ड मौजूद नहीं है।" + }, + { + "id": "api.team.import_team.no_import_from.app_error", + "translation": "विकृत अनुरोध: importFrom मौजूद नहीं है।" + }, + { + "id": "api.slackimport.slack_add_users.merge_existing", + "translation": "मेल खाने वाले ईमेल {{.Email}} और उपयोगकर्ता नाम {{.Username}} के साथ स्लैक उपयोगकर्ता का मौजूदा मैटरमोस्ट उपयोगकर्ता के साथ विलय हो गया।\n" + }, + { + "id": "api.server.warn_metric.number_of_teams_5.contact_us.email_body", + "translation": "सबसे महत्वपूर्ण हमसे संपर्क करें अनुरोध। मुझे टीम योजनाओं के साथ उन्नत अनुमतियों के बारे में अधिक जानने में दिलचस्पी है।\n" + }, + { + "id": "api.server.warn_metric.mfa.notification_title", + "translation": "बहु-कारक प्रमाणीकरण लागू करना" + }, + { + "id": "api.server.warn_metric.mfa.contact_us.email_body", + "translation": "सबसे महत्वपूर्ण हमसे संपर्क करें अनुरोध। मुझे मल्टी-फैक्टर ऑथेंटिकेशन लागू करने के बारे में और जानने में दिलचस्पी है।\n" + }, + { + "id": "api.post.check_for_out_of_channel_group_users.message.none", + "translation": "@{{.GroupName}} का इस टीम में कोई सदस्य नहीं है" + }, + { + "id": "api.email_batching.add_notification_email_to_batch.channel_full.app_error", + "translation": "ईमेल बैचिंग कार्य का प्राप्तकर्ता चैनल भरा हुआ था। कृपया EmailBatchingBufferSize बढ़ाएँ।" + }, + { + "id": "api.context.token_provided.app_error", + "translation": "सत्र OAuth नहीं है लेकिन क्वेरी स्ट्रिंग में टोकन प्रदान किया गया था।" + }, + { + "id": "app.job.get_count_by_status_and_type.app_error", + "translation": "स्थिति और प्रकार के अनुसार कार्य गणना प्राप्त करने में असमर्थ।" + }, + { + "id": "app.job.get_all.app_error", + "translation": "नौकरी नहीं मिल पा रही है।" + }, + { + "id": "app.job.get.app_error", + "translation": "नौकरी नहीं मिल पा रही है।" + }, + { + "id": "app.job.download_export_results_not_enabled", + "translation": "config.json में DownloadExportResults गलत है। इस नौकरी के परिणाम डाउनलोड करने के लिए कृपया इसे सही पर सेट करें।" + }, + { + "id": "app.import.validate_user_teams_import_data.team_name_missing.error", + "translation": "उपयोगकर्ता की टीम सदस्यता से टीम का नाम गायब है।" + }, + { + "id": "app.import.validate_user_teams_import_data.invalid_team_theme.error", + "translation": "उपयोगकर्ता के लिए अमान्य टीम थीम" + }, + { + "id": "app.import.validate_user_teams_import_data.invalid_roles.error", + "translation": "उपयोगकर्ता की टीम सदस्यता के लिए अमान्य भूमिकाएँ।" + }, + { + "id": "app.import.validate_user_import_data.username_missing.error", + "translation": "गुम उपयोगकर्ता संपत्ति की आवश्यकता है: उपयोगकर्ता नाम।" + }, + { + "id": "app.import.validate_user_import_data.username_invalid.error", + "translation": "आपका यूसरनेम गलत है।" + }, + { + "id": "app.import.validate_user_import_data.roles_invalid.error", + "translation": "उपयोगकर्ता भूमिकाएं मान्य नहीं हैं।" + }, + { + "id": "app.import.validate_user_import_data.profile_image.error", + "translation": "अमान्य प्रोफ़ाइल चित्र।" + }, + { + "id": "app.import.validate_user_import_data.position_length.error", + "translation": "उपयोगकर्ता स्थिति बहुत लंबी है।" + }, + { + "id": "app.import.validate_user_import_data.password_length.error", + "translation": "उपयोगकर्ता पासवर्ड की लंबाई अमान्य है।" + }, + { + "id": "app.import.validate_user_import_data.notify_props_mobile_push_status_invalid.error", + "translation": "अमान्य मोबाइल पुश स्थिति उपयोगकर्ता के लिए प्रोप को सूचित करें।" + }, + { + "id": "app.import.validate_user_import_data.notify_props_mobile_invalid.error", + "translation": "अमान्य मोबाइल उपयोगकर्ता के लिए प्रोप मूल्य सूचित करें।" + }, + { + "id": "app.import.validate_user_import_data.notify_props_email_invalid.error", + "translation": "अमान्य ईमेल उपयोगकर्ता के लिए प्रोप मान को सूचित करें।" + }, + { + "id": "app.import.validate_user_import_data.notify_props_desktop_sound_invalid.error", + "translation": "अमान्य डेस्कटॉप ध्वनि उपयोगकर्ता के लिए Prop मान को सूचित करें।" + }, + { + "id": "app.import.validate_user_import_data.notify_props_desktop_invalid.error", + "translation": "अमान्य डेस्कटॉप उपयोगकर्ता के लिए Prop मान को सूचित करें।" + }, + { + "id": "app.import.validate_user_import_data.notify_props_comments_trigger_invalid.error", + "translation": "उपयोगकर्ता के लिए अमान्य टिप्पणियाँ प्रोप मान।" + }, + { + "id": "app.import.validate_user_import_data.notify_props_channel_trigger_invalid.error", + "translation": "अमान्य चैनल ट्रिगर उपयोगकर्ता के लिए प्रोप को सूचित करें।" + }, + { + "id": "app.import.validate_user_import_data.nickname_length.error", + "translation": "उपयोगकर्ता उपनाम बहुत लंबा है।" + }, + { + "id": "app.import.validate_user_import_data.last_name_length.error", + "translation": "उपयोगकर्ता उपनाम बहुत लंबा है।" + }, + { + "id": "app.import.validate_user_import_data.first_name_length.error", + "translation": "उपयोगकर्ता प्रथम नाम बहुत लंबा है." + }, + { + "id": "app.import.validate_user_import_data.email_missing.error", + "translation": "आवश्यक उपयोगकर्ता संपत्ति गुम है: ईमेल।" + }, + { + "id": "app.import.validate_user_import_data.email_length.error", + "translation": "उपयोगकर्ता ईमेल की लंबाई अमान्य है।" + }, + { + "id": "app.import.validate_user_import_data.auth_data_length.error", + "translation": "उपयोगकर्ता AuthData बहुत लंबा है।" + }, + { + "id": "app.import.validate_user_import_data.auth_data_and_service_dependency.error", + "translation": "उपयोगकर्ता AuthService और AuthData परस्पर समावेशी हैं।" + }, + { + "id": "app.import.validate_user_import_data.auth_data_and_password.error", + "translation": "उपयोगकर्ता AuthData और पासवर्ड परस्पर अनन्य हैं।" + }, + { + "id": "app.notification.body.dm.subTitle", + "translation": "जब आप दूर थे, तब {{.SenderName}} ने आपको एक नया सीधा संदेश भेजा था।" + }, + { + "id": "app.job.update.app_error", + "translation": "कार्य अद्यतन करने में असमर्थ।" + }, + { + "id": "app.job.save.app_error", + "translation": "कार्य सहेजने में असमर्थ।" + }, + { + "id": "app.job.get_newest_job_by_status_and_type.app_error", + "translation": "स्थिति और प्रकार के आधार पर नवीनतम नौकरी पाने में असमर्थ।" + }, + { + "id": "app.notification.footer.title", + "translation": "अपनी सूचना सेटिंग बदलना चाहते हैं?" + }, + { + "id": "app.notification.footer.infoLogin", + "translation": "मैटरमोस्ट में लॉगिन करें" + }, + { + "id": "app.notification.footer.info", + "translation": " और सेटिंग> नोटिफिकेशन पर जाएं" + }, + { + "id": "app.notification.body.thread_gm.subTitle", + "translation": "जब आप दूर थे, {{.SenderName}} ने आपके समूह में एक सूत्र का उत्तर दिया।" + }, + { + "id": "app.notification.body.thread_dm.subTitle", + "translation": "जब आप दूर थे, {{.SenderName}} ने आपके सीधे संदेश में एक सूत्र का उत्तर दिया।" + }, + { + "id": "app.notification.body.thread_channel_full.subTitle", + "translation": "जब आप दूर थे, {{.SenderName}} ने उस थ्रेड का उत्तर दिया जिसका आप {{.ChannelName}} में अनुसरण कर रहे हैं।" + }, + { + "id": "app.notification.body.thread_channel.subTitle", + "translation": "जब आप दूर थे, {{.SenderName}} ने आपके द्वारा अनुसरण किए जा रहे थ्रेड का उत्तर दिया।" + }, + { + "id": "app.notification.body.thread.title", + "translation": "{{.SenderName}} ने एक सूत्र का उत्तर दिया" + }, + { + "id": "app.notification.body.mention.title", + "translation": "{{.SenderName}} ने एक संदेश में आपका उल्लेख किया" + }, + { + "id": "app.notification.body.mention.subTitle", + "translation": "जब आप दूर थे, {{.SenderName}} ने {{.ChannelName}} चैनल में आपका उल्लेख किया।" + }, + { + "id": "app.notification.body.group.title", + "translation": "{{.SenderName}} ने आपको एक नया संदेश भेजा है" + }, + { + "id": "app.notification.body.group.subTitle", + "translation": "जब आप दूर थे, {{.SenderName}} ने आपके समूह को एक संदेश भेजा।" + }, + { + "id": "app.notification.body.dm.title", + "translation": "{{.SenderName}} ने आपको एक नया संदेश भेजा है" + }, + { + "id": "app.user_access_token.delete.app_error", + "translation": "व्यक्तिगत पहुंच टोकन को हटाने में असमर्थ।" + }, + { + "id": "app.user.get_thread_membership_for_user.app_error", + "translation": "उपयोगकर्ता थ्रेड सदस्यता प्राप्त करने में असमर्थ" + }, + { + "id": "app.team.get_member.app_error", + "translation": "टीम के सदस्य प्राप्त करने में असमर्थ।" + }, + { + "id": "app.system.permanent_delete_by_name.app_error", + "translation": "हम सिस्टम तालिका प्रविष्टि को स्थायी रूप से हटा नहीं सके।" + }, + { + "id": "app.user_access_token.get_all.app_error", + "translation": "सभी व्यक्तिगत पहुंच टोकन प्राप्त करने में असमर्थ।" + }, + { + "id": "app.user_access_token.disabled", + "translation": "इस सर्वर पर व्यक्तिगत पहुंच टोकन अक्षम हैं। विवरण के लिए कृपया अपने सिस्टम व्यवस्थापक से संपर्क करें।" + }, + { + "id": "app.user.send_auto_response.app_error", + "translation": "उपयोगकर्ता से स्वतः प्रतिक्रिया भेजने में असमर्थ।" + }, + { + "id": "app.user.permanentdeleteuser.internal_error", + "translation": "उपयोगकर्ता को हटाने में असमर्थ।" + }, + { + "id": "app.user.get_thread_membership_for_user.not_found", + "translation": "उपयोगकर्ता थ्रेड सदस्यता मौजूद नहीं है" + }, + { + "id": "app.user.get_thread_count_for_user.app_error", + "translation": "हम उपयोगकर्ता के लिए थ्रेड काउंट प्राप्त नहीं कर सके।" + }, + { + "id": "app.user.convert_bot_to_user.app_error", + "translation": "बॉट को उपयोगकर्ता में बदलने में असमर्थ।" + }, + { + "id": "app.upload.upload_data.update.app_error", + "translation": "अपलोड सत्र को अपडेट करने में विफल।" + }, + { + "id": "app.upload.upload_data.save.app_error", + "translation": "फ़ाइल जानकारी सहेजने में विफल।" + }, + { + "id": "app.upload.upload_data.read_file.app_error", + "translation": "फ़ाइल पढ़ने में विफल।" + }, + { + "id": "app.upload.upload_data.large_image.app_error", + "translation": "{{.Filename}} आयाम ({{.Width}} गुणा {{.Height}} पिक्सल) सीमा से अधिक हैं।" + }, + { + "id": "app.upload.upload_data.first_part_too_small.app_error", + "translation": "डेटा अपलोड करने में विफल. पहला भाग कम से कम {{.Size}} बाइट्स का होना चाहिए।" + }, + { + "id": "app.upload.upload_data.concurrent.app_error", + "translation": "एक से अधिक अनुरोध से डेटा अपलोड करने में असमर्थ।" + }, + { + "id": "app.upload.run_plugins_hook.rejected", + "translation": "फ़ाइल अपलोड करने में असमर्थ {{.Filename}}. प्लगइन द्वारा अस्वीकृत: {{.Reason}}" + }, + { + "id": "app.upload.run_plugins_hook.move_fail", + "translation": "फ़ाइल ले जाने में विफल।" + }, + { + "id": "app.upload.get_for_user.app_error", + "translation": "उपयोगकर्ता के लिए अपलोड प्राप्त करने में विफल।" + }, + { + "id": "app.upload.get.app_error", + "translation": "अपलोड प्राप्त करने में विफल।" + }, + { + "id": "app.upload.create.save.app_error", + "translation": "अपलोड सहेजने में विफल।" + }, + { + "id": "app.upload.create.incorrect_channel_id.app_error", + "translation": "निर्दिष्ट चैनल पर अपलोड नहीं किया जा सकता।" + }, + { + "id": "app.upload.create.cannot_upload_to_deleted_channel.app_error", + "translation": "हटाए गए चैनल पर अपलोड नहीं किया जा सकता।" + }, + { + "id": "app.terms_of_service.get.no_rows.app_error", + "translation": "सेवा की कोई शर्तें नहीं मिलीं।" + }, + { + "id": "app.terms_of_service.get.app_error", + "translation": "सेवा की शर्तें लाने में असमर्थ।" + }, + { + "id": "app.terms_of_service.create.existing.app_error", + "translation": "मौजूदा सेवा की शर्तों के लिए सेव को कॉल नहीं करना चाहिए।" + }, + { + "id": "app.terms_of_service.create.app_error", + "translation": "सेवा की शर्तें सहेजने में असमर्थ।" + }, + { + "id": "app.team.user_belongs_to_teams.app_error", + "translation": "यह निर्धारित करने में असमर्थ कि उपयोगकर्ता टीमों की सूची से संबंधित है या नहीं।" + }, + { + "id": "app.team.update.updating.app_error", + "translation": "टीम को अपडेट करने में हमें एक त्रुटि का सामना करना पड़ा।" + }, + { + "id": "app.team.update.find.app_error", + "translation": "अद्यतन करने के लिए मौजूदा टीम को खोजने में असमर्थ।" + }, + { + "id": "app.team.search_private_team.app_error", + "translation": "निजी टीमों को खोजने में हमें एक त्रुटि का सामना करना पड़ा।" + }, + { + "id": "app.team.search_open_team.app_error", + "translation": "खुली टीमों को खोजने में हमें एक त्रुटि का सामना करना पड़ा।" + }, + { + "id": "app.team.search_all_team.app_error", + "translation": "हमें टीमों को खोजने में एक त्रुटि का सामना करना पड़ा।" + }, + { + "id": "app.team.save_member.save.app_error", + "translation": "टीम के सदस्य को सहेजने में असमर्थ।" + }, + { + "id": "app.team.save.existing.app_error", + "translation": "मौजूदा टीम के लिए अपडेट कॉल करना चाहिए।" + }, + { + "id": "app.team.save.app_error", + "translation": "टीम को बचाने में असमर्थ।" + }, + { + "id": "app.team.reset_all_team_schemes.app_error", + "translation": "हम टीम योजनाओं को रीसेट नहीं कर सके।" + }, + { + "id": "app.team.rename_team.name_occupied", + "translation": "टीम का नाम बदलने में असमर्थ, नाम पहले से प्रयोग में है।" + }, + { + "id": "app.team.remove_member.app_error", + "translation": "टीम के सदस्य को निकालने में असमर्थ।" + }, + { + "id": "app.team.permanentdeleteteam.internal_error", + "translation": "टीम को हटाने में असमर्थ।" + }, + { + "id": "app.team.permanent_delete.app_error", + "translation": "मौजूदा टीम को हटाने में असमर्थ।" + }, + { + "id": "app.team.migrate_team_members.update.app_error", + "translation": "टीम के सदस्य को अपडेट करने में विफल।" + }, + { + "id": "app.team.join_user_to_team.save_member.max_accounts.app_error", + "translation": "नई टीम सदस्यता बनाने में असमर्थ क्योंकि टीम सदस्यों की सीमा तक पहुंच गई है" + }, + { + "id": "app.team.join_user_to_team.save_member.conflict.app_error", + "translation": "नई टीम सदस्यता बनाने में असमर्थ क्योंकि यह पहले से मौजूद है" + }, + { + "id": "app.team.join_user_to_team.save_member.app_error", + "translation": "नई टीम सदस्यता बनाने में असमर्थ" + }, + { + "id": "app.team.join_user_to_team.max_accounts.app_error", + "translation": "यह टीम अनुमत खातों की अधिकतम संख्या तक पहुंच गई है। उच्च सीमा निर्धारित करने के लिए अपने सिस्टम व्यवस्थापक से संपर्क करें।" + }, + { + "id": "app.team.invite_token.group_constrained.error", + "translation": "टोकन द्वारा समूह-विवश टीम में शामिल होने में असमर्थ।" + }, + { + "id": "app.team.invite_id.group_constrained.error", + "translation": "आमंत्रण द्वारा समूह-विवश टीम में शामिल होने में असमर्थ।" + }, + { + "id": "app.team.get_user_team_ids.app_error", + "translation": "उपयोगकर्ता की टीमों की सूची प्राप्त करने में असमर्थ।" + }, + { + "id": "app.team.get_unread.app_error", + "translation": "टीमों को अपठित संदेश प्राप्त करने में असमर्थ।" + }, + { + "id": "app.team.get_members_by_ids.app_error", + "translation": "टीम के सदस्यों को प्राप्त करने में असमर्थ।" + }, + { + "id": "app.team.get_members.app_error", + "translation": "टीम के सदस्यों को प्राप्त करने में असमर्थ।" + }, + { + "id": "app.team.get_member_count.app_error", + "translation": "टीम के सदस्यों की गिनती करने में असमर्थ।" + }, + { + "id": "app.team.get_member.missing.app_error", + "translation": "उस यूजर आईडी और टीम आईडी के लिए टीम का कोई सदस्य नहीं मिला।" + }, + { + "id": "app.team.get_common_team_ids_for_users.app_error", + "translation": "सामान्य टीम आईडी प्राप्त करने में असमर्थ।" + }, + { + "id": "app.team.get_by_scheme.app_error", + "translation": "प्रदान की गई योजना के लिए चैनल प्राप्त करने में असमर्थ।" + }, + { + "id": "app.team.get_by_name.missing.app_error", + "translation": "मौजूदा टीम को खोजने में असमर्थ।" + }, + { + "id": "app.team.get_by_name.app_error", + "translation": "मौजूदा टीम को खोजने में असमर्थ।" + }, + { + "id": "app.team.get_by_invite_id.finding.app_error", + "translation": "मौजूदा टीम को खोजने में असमर्थ।" + }, + { + "id": "app.team.get_all_team_listing.app_error", + "translation": "हमें सभी टीमें नहीं मिलीं।" + }, + { + "id": "app.team.get_all_private_team_listing.app_error", + "translation": "हमें सभी निजी टीमें नहीं मिलीं।" + }, + { + "id": "app.team.get_all.app_error", + "translation": "हमें सभी टीमें नहीं मिलीं।" + }, + { + "id": "app.team.get_active_member_count.app_error", + "translation": "टीम के सदस्यों की गिनती करने में असमर्थ।" + }, + { + "id": "app.team.get.finding.app_error", + "translation": "हमें टीम खोजने में एक त्रुटि का सामना करना पड़ा।" + }, + { + "id": "app.team.get.find.app_error", + "translation": "मौजूदा टीम को खोजने में असमर्थ।" + }, + { + "id": "app.team.clear_all_custom_role_assignments.select.app_error", + "translation": "टीम के सदस्यों को पुनः प्राप्त करने में विफल।" + }, + { + "id": "app.team.analytics_team_count.app_error", + "translation": "टीमों की गिनती करने में असमर्थ।" + }, + { + "id": "app.system_install_date.parse_int.app_error", + "translation": "स्थापना तिथि को पार्स करने में विफल।" + }, + { + "id": "app.system.warn_metric.store.app_error", + "translation": "{{.WarnMetricName}} के लिए मान संग्रहीत करने में विफल" + }, + { + "id": "app.system.warn_metric.bot_displayname", + "translation": "मैटरमोस्ट सलाहकार" + }, + { + "id": "app.system.system_bot.bot_displayname", + "translation": "प्रणाली" + }, + { + "id": "app.system.save.app_error", + "translation": "सिस्टम गुण को सहेजने में हमें एक त्रुटि का सामना करना पड़ा।" + }, + { + "id": "app.user_terms_of_service.get_by_user.app_error", + "translation": "सेवा की शर्तें लाने में असमर्थ।" + }, + { + "id": "app.user_terms_of_service.delete.app_error", + "translation": "सेवा की शर्तें हटाने में असमर्थ।" + }, + { + "id": "app.user_access_token.update_token_enable.app_error", + "translation": "पहुंच टोकन सक्षम करने में असमर्थ।" + }, + { + "id": "app.user_access_token.update_token_disable.app_error", + "translation": "पहुंच टोकन अक्षम करने में असमर्थ।" + }, + { + "id": "app.user_access_token.search.app_error", + "translation": "हमें उपयोगकर्ता पहुंच टोकन खोजने में एक त्रुटि का सामना करना पड़ा।" + }, + { + "id": "app.user_access_token.save.app_error", + "translation": "व्यक्तिगत पहुंच टोकन सहेजने में असमर्थ।" + }, + { + "id": "app.user_access_token.invalid_or_missing", + "translation": "अमान्य या अनुपलब्ध टोकन।" + }, + { + "id": "app.user_access_token.get_by_user.app_error", + "translation": "उपयोगकर्ता द्वारा व्यक्तिगत पहुंच टोकन प्राप्त करने में असमर्थ।" + }, + { + "id": "migrations.worker.run_migration.unknown_key", + "translation": "अज्ञात माइग्रेशन कुंजी के कारण माइग्रेशन कार्य चलाने में असमर्थ।" + }, + { + "id": "migrations.worker.run_advanced_permissions_phase_2_migration.invalid_progress", + "translation": "अमान्य प्रगति डेटा के कारण माइग्रेशन विफल रहा।" + }, + { + "id": "migrations.system.save.app_error", + "translation": "सिस्टम गुण को सहेजने में हमें एक त्रुटि का सामना करना पड़ा।" + }, + { + "id": "mfa.validate_token.authenticate.app_error", + "translation": "अमान्य एमएफए टोकन।" + }, + { + "id": "mfa.mfa_disabled.app_error", + "translation": "इस सर्वर पर बहु-कारक प्रमाणीकरण अक्षम कर दिया गया है।" + }, + { + "id": "mfa.generate_qr_code.create_code.app_error", + "translation": "क्यूआर कोड जनरेट करने में त्रुटि।" + }, + { + "id": "mfa.activate.bad_token.app_error", + "translation": "अमान्य एमएफए टोकन।" + }, + { + "id": "mattermost.bulletin.subject", + "translation": "सबसे महत्वपूर्ण सुरक्षा बुलेटिन" + }, + { + "id": "manaultesting.test_autolink.unable.app_error", + "translation": "चैनल प्राप्त करने में असमर्थ।" + }, + { + "id": "manaultesting.manual_test.parse.app_error", + "translation": "यूआरएल पार्स करने में असमर्थ।" + }, + { + "id": "jobs.set_job_error.update.error", + "translation": "नौकरी की स्थिति को त्रुटि पर सेट करने में विफल" + }, + { + "id": "jobs.request_cancellation.status.error", + "translation": "नौकरी के लिए रद्दीकरण का अनुरोध नहीं किया जा सका जो रद्द करने योग्य स्थिति में नहीं है।" + }, + { + "id": "interactive_message.generate_trigger_id.signing_failed", + "translation": "सहभागी संवाद के लिए उत्पन्न ट्रिगर आईडी पर हस्ताक्षर करने में विफल।" + }, + { + "id": "interactive_message.decode_trigger_id.verify_signature_failed", + "translation": "इंटरैक्टिव डायलॉग के लिए ट्रिगर आईडी का हस्ताक्षर सत्यापन विफल रहा।" + }, + { + "id": "interactive_message.decode_trigger_id.signature_decode_failed", + "translation": "इंटरैक्टिव संवाद के लिए ट्रिगर आईडी के बेस 64 हस्ताक्षर को डीकोड करने में विफल।" + }, + { + "id": "interactive_message.decode_trigger_id.missing_data", + "translation": "इंटरैक्टिव डायलॉग के लिए ट्रिगर आईडी में ज़रूरी डेटा नहीं है." + }, + { + "id": "interactive_message.decode_trigger_id.expired", + "translation": "सहभागी संवाद के लिए ट्रिगर आईडी की समय-सीमा समाप्त हो गई है। ट्रिगर आईडी अधिकतम {{.Seconds}} सेकंड के लिए लाइव होते हैं।" + }, + { + "id": "interactive_message.decode_trigger_id.base64_decode_failed_signature", + "translation": "इंटरैक्टिव संवाद के लिए ट्रिगर आईडी के बेस 64 हस्ताक्षर को डीकोड करने में विफल।" + }, + { + "id": "interactive_message.decode_trigger_id.base64_decode_failed", + "translation": "सहभागी संवाद के लिए ट्रिगर आईडी के लिए बेस 64 को डीकोड करने में विफल।" + }, + { + "id": "groups.unsupported_syncable_type", + "translation": "असमर्थित सिंक करने योग्य प्रकार '{{.Value}}'।" + }, + { + "id": "group_not_associated_to_synced_team", + "translation": "समूह को चैनल से तब तक संबद्ध नहीं किया जा सकता जब तक कि वह पहले पैरेंट समूह-समन्वयित टीम से संबद्ध नहीं हो जाता।" + }, + { + "id": "ent.user.complete_switch_with_oauth.blank_email.app_error", + "translation": "एक खाली ईमेल पते के साथ SAML लॉगिन पूरा करने में असमर्थ।" + }, + { + "id": "ent.saml.service_disable.app_error", + "translation": "SAML 2.0 इस सर्वर पर कॉन्फ़िगर या समर्थित नहीं है।" + }, + { + "id": "ent.saml.save_user.username_exists.saml_app_error", + "translation": "उस उपयोगकर्ता नाम वाला एक खाता पहले से मौजूद है। कृपया अपने व्यवस्थापक से संपर्क करें।" + }, + { + "id": "ent.saml.save_user.email_exists.saml_app_error", + "translation": "यह खाता SAML प्रमाणीकरण का उपयोग नहीं करता है। कृपया ईमेल और पासवर्ड का उपयोग करके साइन इन करें।" + }, + { + "id": "ent.saml.metadata.app_error", + "translation": "सेवा प्रदाता मेटाडेटा बनाते समय कोई त्रुटि उत्पन्न हुई।" + }, + { + "id": "ent.saml.license_disable.app_error", + "translation": "आपका लाइसेंस SAML प्रमाणीकरण का समर्थन नहीं करता है।" + }, + { + "id": "ent.saml.do_login.validate.app_error", + "translation": "पहचान प्रदाता से प्रतिक्रिया की पुष्टि करते समय एक त्रुटि उत्पन्न हुई। कृपया अपने सिस्टम व्यवस्थापक से संपर्क करें।" + }, + { + "id": "ent.saml.do_login.parse.app_error", + "translation": "पहचान प्रदाता से प्रतिक्रिया को पार्स करते समय एक त्रुटि उत्पन्न हुई। कृपया अपने सिस्टम व्यवस्थापक से संपर्क करें।" + }, + { + "id": "ent.saml.do_login.invalid_time.app_error", + "translation": "हमें पहचान प्रदाता से प्रतिक्रिया में एक अमान्य समय प्राप्त हुआ। कृपया अपने सिस्टम व्यवस्थापक से संपर्क करें।" + }, + { + "id": "ent.saml.do_login.invalid_signature.app_error", + "translation": "हमें पहचान प्रदाता से प्रतिक्रिया में एक अमान्य हस्ताक्षर प्राप्त हुआ। कृपया अपने सिस्टम व्यवस्थापक से संपर्क करें।" + }, + { + "id": "ent.saml.do_login.empty_response.app_error", + "translation": "हमें पहचान प्रदाता से एक खाली प्रतिक्रिया मिली।" + }, + { + "id": "ent.saml.do_login.decrypt.app_error", + "translation": "SAML लॉगिन असफल रहा क्योंकि पहचान प्रदाता से प्रतिक्रिया को डिक्रिप्ट करते समय एक त्रुटि हुई। कृपया अपने सिस्टम व्यवस्थापक से संपर्क करें।" + }, + { + "id": "ent.saml.configure.not_encrypted_response.app_error", + "translation": "SAML लॉगिन असफल रहा क्योंकि पहचान प्रदाता प्रतिक्रिया एन्क्रिप्टेड नहीं है। कृपया अपने सिस्टम व्यवस्थापक से संपर्क करें।" + }, + { + "id": "ent.saml.configure.load_private_key.app_error", + "translation": "SAML लॉगिन असफल रहा क्योंकि सेवा प्रदाता निजी कुंजी नहीं मिली थी। कृपया अपने सिस्टम व्यवस्थापक से संपर्क करें।" + }, + { + "id": "ent.saml.configure.load_idp_cert.app_error", + "translation": "पहचान प्रदाता सार्वजनिक प्रमाणपत्र फ़ाइल नहीं मिली। कृपया अपने सिस्टम व्यवस्थापक से संपर्क करें।" + }, + { + "id": "ent.saml.configure.encryption_not_enabled.app_error", + "translation": "SAML लॉगिन असफल रहा क्योंकि एन्क्रिप्शन सक्षम नहीं है। कृपया अपने सिस्टम व्यवस्थापक से संपर्क करें।" + }, + { + "id": "ent.saml.build_request.encoding.app_error", + "translation": "पहचान प्रदाता के लिए अनुरोध को कूटबद्ध करते समय एक त्रुटि उत्पन्न हुई। कृपया अपने सिस्टम व्यवस्थापक से संपर्क करें।" + }, + { + "id": "ent.saml.build_request.app_error", + "translation": "पहचान प्रदाता के लिए अनुरोध शुरू करते समय एक त्रुटि उत्पन्न हुई। कृपया अपने सिस्टम व्यवस्थापक से संपर्क करें।" + }, + { + "id": "ent.saml.attribute.app_error", + "translation": "SAML लॉगिन असफल रहा क्योंकि विशेषताओं में से एक गलत है। कृपया अपने सिस्टम व्यवस्थापक से संपर्क करें।" + }, + { + "id": "ent.migration.migratetosaml.username_already_used_by_other_user", + "translation": "उपयोगकर्ता नाम पहले से ही किसी अन्य मैटरमॉस्ट उपयोगकर्ता द्वारा उपयोग किया जा रहा है।" + }, + { + "id": "ent.migration.migratetosaml.user_not_found_in_users_mapping_file", + "translation": "उपयोगकर्ता फ़ाइल में उपयोगकर्ता नहीं मिला।" + }, + { + "id": "ent.migration.migratetosaml.email_already_used_by_other_user", + "translation": "ईमेल पहले से ही किसी अन्य SAML उपयोगकर्ता द्वारा उपयोग किया जा रहा है।" + }, + { + "id": "ent.migration.migratetoldap.user_not_found", + "translation": "AD/LDAP सर्वर पर उपयोगकर्ता ढूँढने में असमर्थ: " + }, + { + "id": "ent.migration.migratetoldap.duplicate_field", + "translation": "निर्दिष्ट फ़ील्ड के साथ AD/LDAP उपयोगकर्ताओं को माइग्रेट करने में असमर्थ। डुप्लिकेट प्रविष्टि का पता चला। कृपया सभी डुप्लिकेट निकालें और पुनः प्रयास करें।" + }, + { + "id": "ent.message_export.run_export.app_error", + "translation": "संदेश निर्यात डेटा का चयन करने में विफल।" + }, + { + "id": "ent.message_export.global_relay_export.get_attachment_error", + "translation": "किसी पोस्ट के लिए फ़ाइल जानकारी प्राप्त करने में विफल।" + }, + { + "id": "ent.message_export.global_relay_export.deliver.unable_to_open_zip_file_data.app_error", + "translation": "निर्यात अस्थायी फ़ाइल को खोलने में असमर्थ।" + }, + { + "id": "ent.message_export.global_relay_export.deliver.unable_to_open_email_file.app_error", + "translation": "अस्थायी फ़ाइल से ईमेल प्राप्त करने में असमर्थ।" + }, + { + "id": "ent.message_export.global_relay_export.deliver.unable_to_get_file_info.app_error", + "translation": "निर्यात अस्थायी फ़ाइल की जानकारी प्राप्त करने में असमर्थ।" + }, + { + "id": "ent.message_export.global_relay_export.deliver.to_address.app_error", + "translation": "पते पर ईमेल सेट करने में असमर्थ।" + }, + { + "id": "ent.message_export.global_relay_export.deliver.parse_mail.app_error", + "translation": "ईमेल जानकारी पढ़ने में असमर्थ।" + }, + { + "id": "ent.message_export.global_relay_export.deliver.msg_data.app_error", + "translation": "ईमेल संदेश लिखने में असमर्थ।" + }, + { + "id": "ent.message_export.global_relay_export.deliver.msg.app_error", + "translation": "ईमेल संदेश सेट करने में असमर्थ." + }, + { + "id": "ent.message_export.global_relay_export.deliver.from_address.app_error", + "translation": "ईमेल पते से सेट करने में असमर्थ।" + }, + { + "id": "ent.message_export.global_relay_export.deliver.close.app_error", + "translation": "ग्लोबल रिले को ईमेल डिलीवर करने में असमर्थ।" + }, + { + "id": "ent.message_export.global_relay.generate_email.app_error", + "translation": "ईएमएल फ़ाइल डेटा जनरेट करने में असमर्थ।" + }, + { + "id": "ent.message_export.global_relay.create_file_in_zip.app_error", + "translation": "ईएमएल फ़ाइल बनाने में असमर्थ।" + }, + { + "id": "ent.message_export.global_relay.close_zip_file.app_error", + "translation": "ज़िप फ़ाइल बंद करने में असमर्थ." + }, + { + "id": "ent.message_export.global_relay.attach_file.app_error", + "translation": "ग्लोबल रिले एक्सपोर्ट में अटैचमेंट जोड़ने में असमर्थ।" + }, + { + "id": "ent.message_export.csv_export.get_attachment_error", + "translation": "किसी पोस्ट के लिए फ़ाइल जानकारी प्राप्त करने में विफल." + }, + { + "id": "ent.message_export.actiance_export.get_attachment_error", + "translation": "किसी पोस्ट के लिए फ़ाइल जानकारी प्राप्त करने में विफल।" + }, + { + "id": "ent.ldap_id_migrate.app_error", + "translation": "माइग्रेट करने में असमर्थ।" + }, + { + "id": "ent.ldap_groups.reachable_groups_error", + "translation": "उपयोगकर्ता के लिए समूह पुनर्प्राप्त करने में त्रुटि" + }, + { + "id": "ent.ldap_groups.no_rows", + "translation": "मेल खाने वाले यूआईडी के साथ कोई समूह नहीं मिला" + }, + { + "id": "ent.ldap_groups.members_of_group_error", + "translation": "समूह के सदस्यों को पुनः प्राप्त करने में त्रुटि" + }, + { + "id": "ent.ldap_groups.groups_search_error", + "translation": "एलडीएपी समूह पुनर्प्राप्त करने में त्रुटि" + }, + { + "id": "ent.ldap_groups.group_search_error", + "translation": "एलडीएपी समूह पुनर्प्राप्त करने में त्रुटि" + }, + { + "id": "ent.ldap.validate_guest_filter.app_error", + "translation": "अमान्य AD/LDAP अतिथि फ़िल्टर।" + }, + { + "id": "ent.ldap.validate_filter.app_error", + "translation": "अमान्य AD/LDAP फ़िल्टर।" + }, + { + "id": "ent.ldap.validate_admin_filter.app_error", + "translation": "अमान्य AD/LDAP व्यवस्थापक फ़िल्टर।" + }, + { + "id": "ent.ldap.syncronize.search_failure_size_exceeded.app_error", + "translation": "आकार सीमा पार हो गई। अपने [अधिकतम पृष्ठ आकार](https://docs.mattermost.com/deployment/sso-ldap.html#i-see-the-log-error-ldap-result-code-4-size-limit-exceeded) की जाँच करके देखें।" + }, + { + "id": "ent.ldap.syncronize.search_failure.app_error", + "translation": "AD/LDAP में उपयोगकर्ताओं को खोजने में विफल। परीक्षण करें कि क्या मैटरमोस्ट सर्वर आपके AD/LDAP सर्वर से जुड़ सकता है और पुनः प्रयास करें।" + }, + { + "id": "ent.ldap.syncronize.populate_syncables", + "translation": "सिंकेबल्स को पॉप्युलेट करने में त्रुटि" + }, + { + "id": "ent.ldap.syncronize.get_all_groups.app_error", + "translation": "समूह पुनर्प्राप्त करने में त्रुटि।" + }, + { + "id": "ent.ldap.syncronize.get_all.app_error", + "translation": "AD/LDAP का उपयोग करने वाले सभी उपयोगकर्ताओं को प्राप्त करने में असमर्थ।" + }, + { + "id": "ent.ldap.syncronize.delete_group_constained_memberships", + "translation": "टीम या चैनल सदस्यता हटाने में त्रुटि" + }, + { + "id": "ent.ldap.save_user.username_exists.ldap_app_error", + "translation": "उस उपयोगकर्ता नाम वाला एक खाता पहले से मौजूद है। कृपया अपने व्यवस्थापक से संपर्क करें।" + }, + { + "id": "ent.ldap.save_user.email_exists.ldap_app_error", + "translation": "यह खाता AD/LDAP प्रमाणीकरण का उपयोग नहीं करता है। कृपया ईमेल और पासवर्ड का उपयोग करके साइन इन करें।" + }, + { + "id": "ent.ldap.no.users.checkcertificate", + "translation": "कोई LDAP उपयोगकर्ता नहीं मिला, अपना उपयोगकर्ता फ़िल्टर और प्रमाणपत्र जांचें।" + }, + { + "id": "ent.ldap.do_login.x509.app_error", + "translation": "कुंजी युग्म बनाने में त्रुटि" + }, + { + "id": "ent.ldap.do_login.user_not_registered.app_error", + "translation": "उपयोगकर्ता AD/LDAP सर्वर पर पंजीकृत नहीं है।" + }, + { + "id": "ent.ldap.do_login.user_filtered.app_error", + "translation": "आपके AD/LDAP खाते को इस मैटरमॉस्ट सर्वर का उपयोग करने की अनुमति नहीं है। कृपया अपने सिस्टम व्यवस्थापक से AD/LDAP उपयोगकर्ता फ़िल्टर की जाँच करने के लिए कहें।" + }, + { + "id": "ent.ldap.do_login.unable_to_connect.app_error", + "translation": "AD/LDAP सर्वर से कनेक्ट करने में असमर्थ।" + }, + { + "id": "ent.ldap.do_login.search_ldap_server.app_error", + "translation": "AD/LDAP सर्वर खोजने में विफल।" + }, + { + "id": "ent.ldap.do_login.matched_to_many_users.app_error", + "translation": "दिया गया उपयोगकर्ता नाम एकाधिक उपयोगकर्ताओं से मेल खाता है।" + }, + { + "id": "ent.ldap.do_login.licence_disable.app_error", + "translation": "AD/LDAP कार्यक्षमता वर्तमान लाइसेंस द्वारा अक्षम की गई है। अपने एंटरप्राइज़ लाइसेंस को अपग्रेड करने के बारे में कृपया अपने सिस्टम व्यवस्थापक से संपर्क करें।" + }, + { + "id": "ent.ldap.do_login.key.app_error", + "translation": "LDAP TLS कुंजी फ़ाइल लोड करने में त्रुटि." + }, + { + "id": "ent.ldap.do_login.invalid_password.app_error", + "translation": "अवैध पासवर्ड।" + }, + { + "id": "ent.ldap.do_login.certificate.app_error", + "translation": "LDAP TLS प्रमाणपत्र फ़ाइल लोड करने में त्रुटि।" + }, + { + "id": "ent.ldap.do_login.bind_admin_user.app_error", + "translation": "AD/LDAP सर्वर से जुड़ने में असमर्थ। बाइंड यूज़रनेम और बाइंडपासवर्ड की जाँच करें।" + }, + { + "id": "ent.ldap.disabled.app_error", + "translation": "AD/LDAP अक्षम या लाइसेंस AD/LDAP का समर्थन नहीं करता है।" + }, + { + "id": "ent.ldap.create_fail", + "translation": "LDAP उपयोगकर्ता बनाने में असमर्थ।" + }, + { + "id": "ent.ldap.app_error", + "translation": "ldap इंटरफ़ेस शून्य था।" + }, + { + "id": "ent.jobs.start_synchronize_job.timeout", + "translation": "AD/LDAP सिंक्रोनाइज़ेशन जॉब टाइमआउट पर पहुँच गया।" + }, + { + "id": "ent.id_loaded.license_disable.app_error", + "translation": "आपका लाइसेंस आईडी लोडेड पुश नोटिफिकेशन का समर्थन नहीं करता है।" + }, + { + "id": "ent.get_users_in_channel_during", + "translation": "निर्दिष्ट समयावधि के दौरान चैनल में उपयोगकर्ताओं को लाने में विफल।" + }, + { + "id": "ent.elasticsearch.test_config.reenter_password", + "translation": "Elasticsearch सर्वर URL या उपयोगकर्ता नाम बदल गया है। कृपया कनेक्शन का परीक्षण करने के लिए इलास्टिक्स खोज पासवर्ड फिर से दर्ज करें।" + }, + { + "id": "ent.elasticsearch.test_config.license.error", + "translation": "आपका लाइसेंस इलास्टिक्स खोज का समर्थन नहीं करता है।" + }, + { + "id": "ent.elasticsearch.test_config.indexing_disabled.error", + "translation": "लोचदार खोज अक्षम है।" + }, + { + "id": "ent.elasticsearch.stop.already_stopped.app_error", + "translation": "इलास्टिक्स खोज पहले ही रोक दी गई है।" + }, + { + "id": "ent.elasticsearch.start.start_bulk_processor_failed.app_error", + "translation": "Elasticsearch बल्क प्रोसेसर प्रारंभ करने में विफल।" + }, + { + "id": "ent.elasticsearch.start.parse_server_version.app_error", + "translation": "Elasticsearch सर्वर संस्करण को पार्स करने में विफल।" + }, + { + "id": "ent.elasticsearch.start.get_server_version.app_error", + "translation": "Elasticsearch सर्वर संस्करण प्राप्त करने में विफल।" + }, + { + "id": "ent.elasticsearch.start.create_bulk_processor_failed.app_error", + "translation": "Elasticsearch बल्क प्रोसेसर बनाने में विफल।" + }, + { + "id": "ent.elasticsearch.start.already_started.app_error", + "translation": "इलास्टिक्स खोज पहले ही शुरू हो चुकी है।" + }, + { + "id": "ent.elasticsearch.search_users.unmarshall_user_failed", + "translation": "खोज परिणामों को डीकोड करने में विफल" + }, + { + "id": "ent.elasticsearch.search_users.search_failed", + "translation": "खोज पूरी नहीं हो सकी" + }, + { + "id": "ent.elasticsearch.search_posts.unmarshall_post_failed", + "translation": "खोज परिणामों को डीकोड करने में विफल" + }, + { + "id": "ent.elasticsearch.search_posts.search_failed", + "translation": "खोज पूरी नहीं हो सकी" + }, + { + "id": "ent.elasticsearch.search_posts.parse_matches_failed", + "translation": "खोज परिणाम मिलानों को पार्स करने में विफल" + }, + { + "id": "ent.elasticsearch.search_posts.disabled", + "translation": "इस सर्वर पर इलास्टिक्स खोज अक्षम है" + }, + { + "id": "ent.elasticsearch.search_channels.unmarshall_channel_failed", + "translation": "खोज परिणामों को डीकोड करने में विफल" + }, + { + "id": "ent.elasticsearch.search_channels.search_failed", + "translation": "खोज पूरी नहीं हो सकी" + }, + { + "id": "ent.elasticsearch.search_channels.disabled", + "translation": "इस सर्वर पर इलास्टिक्स खोज अक्षम है" + }, + { + "id": "ent.elasticsearch.refresh_indexes.refresh_failed", + "translation": "Elasticsearch अनुक्रमणिका ताज़ा करने में विफल" + }, + { + "id": "ent.elasticsearch.purge_indexes.delete_failed", + "translation": "इलास्टिक्स खोज इंडेक्स को हटाने में विफल" + }, + { + "id": "ent.elasticsearch.post.get_posts_batch_for_indexing.error", + "translation": "अनुक्रमण के लिए पोस्ट बैच प्राप्त करने में असमर्थ।" + }, + { + "id": "ent.elasticsearch.not_started.error", + "translation": "लोचदार खोज शुरू नहीं हुई है" + }, + { + "id": "ent.elasticsearch.indexer.index_batch.nothing_left_to_index.error", + "translation": "सभी संस्थाओं के पूर्ण होने पर एक नए बैच को अनुक्रमित करने का प्रयास किया जा रहा है" + }, + { + "id": "ent.elasticsearch.indexer.do_job.parse_start_time.error", + "translation": "इलास्टिक्स खोज अनुक्रमण कार्यकर्ता प्रारंभ समय को पार्स करने में विफल रहा" + }, + { + "id": "ent.elasticsearch.indexer.do_job.parse_end_time.error", + "translation": "इलास्टिक्स खोज अनुक्रमण कार्यकर्ता समाप्ति समय को पार्स करने में विफल रहा" + }, + { + "id": "ent.elasticsearch.index_channels_batch.error", + "translation": "अनुक्रमण के लिए चैनल बैच प्राप्त करने में असमर्थ।" + }, + { + "id": "ent.elasticsearch.getAllTeamMembers.error", + "translation": "टीम के सभी सदस्यों को प्राप्त करने में विफल" + }, + { + "id": "ent.elasticsearch.getAllChannelMembers.error", + "translation": "चैनल के सभी सदस्य प्राप्त करने में विफल" + }, + { + "id": "ent.elasticsearch.generic.disabled", + "translation": "इस सर्वर पर इलास्टिक्स खोज सक्षम नहीं है" + }, + { + "id": "ent.elasticsearch.delete_channel_posts.error", + "translation": "चैनल पोस्ट हटाने में विफल" + }, + { + "id": "ent.elasticsearch.delete_channel.error", + "translation": "चैनल को हटाने में विफल" + }, + { + "id": "ent.elasticsearch.data_retention_delete_indexes.get_indexes.error", + "translation": "Elasticsearch अनुक्रमणिका प्राप्त करने में विफल" + }, + { + "id": "ent.elasticsearch.data_retention_delete_indexes.delete_index.error", + "translation": "इलास्टिक्स खोज इंडेक्स को हटाने में विफल" + }, + { + "id": "ent.elasticsearch.create_template_users_if_not_exists.template_create_failed", + "translation": "उपयोगकर्ताओं के लिए इलास्टिक्स खोज टेम्प्लेट बनाने में विफल" + }, + { + "id": "ent.elasticsearch.create_template_posts_if_not_exists.template_create_failed", + "translation": "पदों के लिए इलास्टिक्स खोज टेम्पलेट बनाने में विफल" + }, + { + "id": "ent.elasticsearch.create_template_channels_if_not_exists.template_create_failed", + "translation": "चैनलों के लिए इलास्टिक्स खोज टेम्पलेट बनाने में विफल" + }, + { + "id": "ent.elasticsearch.create_client.connect_failed", + "translation": "Elasticsearch क्लाइंट सेट करना विफल" + }, + { + "id": "ent.elasticsearch.aggregator_worker.index_job_failed.error", + "translation": "अनुक्रमण कार्य विफल होने के कारण इलास्टिक्स खोज एग्रीगेटर कार्यकर्ता विफल रहा" + }, + { + "id": "ent.elasticsearch.aggregator_worker.get_indexes.error", + "translation": "इलास्टिक्स खोज एग्रीगेटर कार्यकर्ता अनुक्रमणिका प्राप्त करने में विफल रहा" + }, + { + "id": "ent.elasticsearch.aggregator_worker.delete_indexes.error", + "translation": "इलास्टिक्स खोज एग्रीगेटर कार्यकर्ता इंडेक्स को हटाने में विफल रहा" + }, + { + "id": "ent.elasticsearch.aggregator_worker.create_index_job.error", + "translation": "इलास्टिक्स खोज एग्रीगेटर कार्यकर्ता अनुक्रमण कार्य बनाने में विफल रहा" + }, + { + "id": "ent.data_retention.run_failed.error", + "translation": "डेटा प्रतिधारण कार्य विफल रहा।" + }, + { + "id": "ent.data_retention.policies.invalid_policy", + "translation": "नीति अमान्य है।" + }, + { + "id": "ent.data_retention.policies.internal_error", + "translation": "हमें अनुरोधित कार्रवाई करने में त्रुटि का सामना करना पड़ा।" + }, + { + "id": "ent.data_retention.generic.license.error", + "translation": "आपका लाइसेंस डेटा प्रतिधारण का समर्थन नहीं करता है।" + }, + { + "id": "ent.compliance.run_failed.error", + "translation": "'{{.JobName}}' कार्य के लिए '{{.FilePath}}' पर अनुपालन निर्यात विफल रहा" + }, + { + "id": "ent.compliance.run_export.template_watcher.appError", + "translation": "निर्यात टेम्प्लेट लोड करने में असमर्थ. कृपया पुन: प्रयास करें।" + }, + { + "id": "ent.compliance.global_relay.rewind_temporary_file.appError", + "translation": "ग्लोबल रिले अस्थायी निर्यात फ़ाइल को फिर से पढ़ने में असमर्थ।" + }, + { + "id": "ent.compliance.csv.zip.creation.appError", + "translation": "ज़िप निर्यात फ़ाइल बनाने में असमर्थ।" + }, + { + "id": "ent.compliance.csv.metadata.json.marshalling.appError", + "translation": "मेटाडेटा को जेसन में बदलने में असमर्थ।" + }, + { + "id": "ent.compliance.csv.metadata.export.appError", + "translation": "ज़िप फ़ाइल में मेटाडेटा फ़ाइल जोड़ने में असमर्थ।" + }, + { + "id": "ent.compliance.csv.header.export.appError", + "translation": "सीएसवी निर्यात में हेडर जोड़ने में असमर्थ।" + }, + { + "id": "ent.compliance.csv.file.creation.appError", + "translation": "अस्थायी CSV निर्यात फ़ाइल बनाने में असमर्थ।" + }, + { + "id": "ent.cluster.404.app_error", + "translation": "क्लस्टर API समापन बिंदु नहीं मिला।" + }, + { + "id": "brand.save_brand_image.check_image_limits.app_error", + "translation": "छवि सीमा जांच विफल। संकल्प बहुत अधिक है।" + }, + { + "id": "bleveengine.purge_user_index.error", + "translation": "उपयोगकर्ता अनुक्रमणिका को शुद्ध करने में विफल।" + }, + { + "id": "bleveengine.purge_post_index.error", + "translation": "पोस्ट इंडेक्स को शुद्ध करने में विफल।" + }, + { + "id": "bleveengine.purge_channel_index.error", + "translation": "चैनल अनुक्रमणिका को शुद्ध करने में विफल।" + }, + { + "id": "bleveengine.indexer.do_job.parse_start_time.error", + "translation": "Bleve अनुक्रमण कार्यकर्ता प्रारंभ समय को पार्स करने में विफल रहा।" + }, + { + "id": "bleveengine.indexer.do_job.parse_end_time.error", + "translation": "Bleve अनुक्रमण कार्यकर्ता समाप्ति समय को पार्स करने में विफल रहा।" + }, + { + "id": "bleveengine.indexer.do_job.engine_inactive", + "translation": "Bleve अनुक्रमणिका कार्य चलाने में विफल: इंजन निष्क्रिय है।" + }, + { + "id": "bleveengine.create_user_index.error", + "translation": "ब्लीव यूजर इंडेक्स बनाने में त्रुटि।" + }, + { + "id": "bleveengine.create_post_index.error", + "translation": "ब्लीव पोस्ट इंडेक्स बनाने में त्रुटि।" + }, + { + "id": "bleveengine.create_channel_index.error", + "translation": "ब्लेव चैनल इंडेक्स बनाने में त्रुटि।" + }, + { + "id": "app.user_terms_of_service.save.app_error", + "translation": "सेवा की शर्तें सहेजने में असमर्थ।" + }, + { + "id": "app.user_terms_of_service.get_by_user.no_rows.app_error", + "translation": "सेवा की कोई शर्तें नहीं मिलीं।" + }, + { + "id": "model.command.is_valid.update_at.app_error", + "translation": "पर अद्यतन एक मान्य समय होना चाहिए।" + }, + { + "id": "model.command.is_valid.create_at.app_error", + "translation": "पर बनाएं एक मान्य समय होना चाहिए।" + }, + { + "id": "model.command.is_valid.autocomplete_data.app_error", + "translation": "अमान्य स्वतपूर्ण डेटा" + }, + { + "id": "model.cluster.is_valid.type.app_error", + "translation": "प्रकार सेट किया जाना चाहिए।" + }, + { + "id": "model.cluster.is_valid.name.app_error", + "translation": "ClusterName सेट किया जाना चाहिए।" + }, + { + "id": "model.cluster.is_valid.last_ping_at.app_error", + "translation": "LastPingAt सेट होना चाहिए।" + }, + { + "id": "model.cluster.is_valid.create_at.app_error", + "translation": "CreateAt सेट होना चाहिए।" + }, + { + "id": "model.channel_member.is_valid.unread_level.app_error", + "translation": "अमान्य मार्क अपठित स्तर।" + }, + { + "id": "model.channel_member.is_valid.notify_level.app_error", + "translation": "अमान्य सूचना स्तर।" + }, + { + "id": "model.channel_member.is_valid.ignore_channel_mentions_value.app_error", + "translation": "अमान्य अनदेखा चैनल स्थिति का उल्लेख करता है।" + }, + { + "id": "model.channel.is_valid.update_at.app_error", + "translation": "पर अद्यतन एक मान्य समय होना चाहिए।" + }, + { + "id": "model.channel.is_valid.name.app_error", + "translation": "अमान्य चैनल नाम. गैर-प्रत्यक्ष संदेश चैनलों के लिए चैनल नाम में उपयोगकर्ता आईडी की अनुमति नहीं है।" + }, + { + "id": "model.channel.is_valid.create_at.app_error", + "translation": "पर बनाएं एक मान्य समय होना चाहिए।" + }, + { + "id": "model.bot.is_valid.update_at.app_error", + "translation": "पर अमान्य अद्यतन।" + }, + { + "id": "model.bot.is_valid.create_at.app_error", + "translation": "अवैध निर्माण पर।" + }, + { + "id": "model.authorize.is_valid.redirect_uri.app_error", + "translation": "अमान्य रीडायरेक्ट uri।" + }, + { + "id": "model.authorize.is_valid.expires.app_error", + "translation": "समाप्त होने वाला है सेट किया जाना चाहिए।" + }, + { + "id": "model.authorize.is_valid.create_at.app_error", + "translation": "पर बनाएं एक मान्य समय होना चाहिए।" + }, + { + "id": "model.access.is_valid.client_id.app_error", + "translation": "अमान्य क्लाइंट आईडी।" + }, + { + "id": "model.access.is_valid.access_token.app_error", + "translation": "अमान्य पहुँच टोकन।" + }, + { + "id": "app.notification.body.dm.time", + "translation": "{{.घंटा}}:{{.मिनट}} {{.समयक्षेत्र}}" + }, + { + "id": "api.user.add_direct_channels_and_forget.failed.error", + "translation": "उपयोगकर्ता user_id={{.UserId}}, team_id={{.TeamId}}, err={{.Error}} के लिए सीधे चैनल वरीयताएँ जोड़ने में विफल" + }, + { + "id": "api.server.warn_metric.email_domain.start_trial_notification_success.message", + "translation": "आपका एंटरप्राइज़ परीक्षण अब सक्रिय है। अतिथि खातों को सक्षम करने के लिए **System Console > Authentication > Guest Access** पर जाएं।" + }, + { + "id": "api.error_set_first_admin_complete_setup", + "translation": "स्टोर में पहले व्यवस्थापक पूर्ण सेटअप को सहेजने का प्रयास करने में त्रुटि।" + }, + { + "id": "model.config.is_valid.saml_spidentifier_attribute.app_error", + "translation": "सेवा प्रदाता पहचानकर्ता आवश्यक है" + }, + { + "id": "model.config.is_valid.saml_private_key.app_error", + "translation": "सेवा प्रदाता निजी कुंजी गायब है। क्या आप इसे अपलोड करना भूल गए?" + }, + { + "id": "model.config.is_valid.saml_idp_descriptor_url.app_error", + "translation": "पहचान प्रदाता जारीकर्ता यूआरएल एक वैध यूआरएल होना चाहिए और http:// या https:// से शुरू होना चाहिए।" + }, + { + "id": "model.config.is_valid.saml_email_attribute.app_error", + "translation": "अमान्य ईमेल विशेषता। सेट होना चाहिए।" + }, + { + "id": "model.config.is_valid.max_notify_per_channel.app_error", + "translation": "टीम सेटिंग के लिए प्रति चैनल अमान्य अधिकतम सूचनाएं। अवश्य ही एक धनात्मक संख्या होनी चाहिए।" + }, + { + "id": "model.config.is_valid.listen_address.app_error", + "translation": "सेवा सेटिंग के लिए अमान्य सुनने का पता सेट किया जाना चाहिए।" + }, + { + "id": "model.config.is_valid.ldap_username", + "translation": "AD/LDAP फ़ील्ड \"उपयोगकर्ता नाम विशेषता\" आवश्यक है।" + }, + { + "id": "model.config.is_valid.ldap_security.app_error", + "translation": "AD/LDAP सेटिंग्स के लिए अमान्य कनेक्शन सुरक्षा। '', 'TLS', या 'STARTTLS' होना चाहिए।" + }, + { + "id": "model.config.is_valid.file_driver.app_error", + "translation": "फ़ाइल सेटिंग्स के लिए अमान्य ड्राइवर नाम। 'स्थानीय' या 'अमेज़ॅन3' होना चाहिए।" + }, + { + "id": "model.config.is_valid.collapsed_threads.autofollow.app_error", + "translation": "संक्षिप्त किए गए थ्रेड को सक्षम करने के लिए ThreadAutoFollow सही होना चाहिए" + }, + { + "id": "model.compliance.is_valid.create_at.app_error", + "translation": "पर बनाएं एक मान्य समय होना चाहिए।" + }, + { + "id": "model.command_hook.create_at.app_error", + "translation": "पर बनाएं एक मान्य समय होना चाहिए।" + }, + { + "id": "app.plugin.marketplace_disabled.app_error", + "translation": "मार्केटप्लेस अक्षम कर दिया गया है. विवरण के लिए कृपया अपने लॉग जांचें।" + }, + { + "id": "app.plugin.marketplace_client.failed_to_fetch", + "translation": "मार्केटप्लेस सर्वर से प्लगइन्स प्राप्त करने में विफल।" + }, + { + "id": "app.plugin.marketplace_client.app_error", + "translation": "मार्केटप्लेस क्लाइंट बनाने में विफल।" + }, + { + "id": "app.plugin.manifest.app_error", + "translation": "निकाले गए प्लगइन के लिए मेनिफेस्ट खोजने में असमर्थ।" + }, + { + "id": "app.plugin.invalid_version.app_error", + "translation": "प्लगइन संस्करण को पार्स नहीं किया जा सका।" + }, + { + "id": "app.plugin.invalid_id.app_error", + "translation": "प्लगइन Id कम से कम {{.Min}} वर्ण, अधिकतम {{.Max}} वर्णों का होना चाहिए और {{.Regex}} से मेल खाना चाहिए।" + }, + { + "id": "app.plugin.install_marketplace_plugin.app_error", + "translation": "मार्केटप्लेस प्लगइन स्थापित करने में विफल।" + }, + { + "id": "app.plugin.install_id_failed_remove.app_error", + "translation": "प्लगइन स्थापित करने में असमर्थ। समान आईडी वाला प्लग इन पहले से ही स्थापित है और निकालने में विफल रहा है।" + }, + { + "id": "app.plugin.install_id.app_error", + "translation": "प्लगइन स्थापित करने में असमर्थ। समान आईडी वाला प्लग इन पहले से ही स्थापित है।" + }, + { + "id": "app.plugin.install.app_error", + "translation": "प्लगइन स्थापित करने में असमर्थ।" + }, + { + "id": "app.plugin.get_statuses.app_error", + "translation": "प्लगइन स्थिति प्राप्त करने में असमर्थ।" + }, + { + "id": "app.plugin.get_public_key.get_file.app_error", + "translation": "स्टोर से सार्वजनिक कुंजी प्राप्त करते समय कोई त्रुटि उत्पन्न हुई।" + }, + { + "id": "app.plugin.get_plugins.app_error", + "translation": "सक्रिय प्लगइन्स प्राप्त करने में असमर्थ।" + }, + { + "id": "app.plugin.get_cluster_plugin_statuses.app_error", + "translation": "क्लस्टर से प्लगइन स्थिति प्राप्त करने में असमर्थ।" + }, + { + "id": "app.plugin.flag_managed.app_error", + "translation": "फ़ाइल स्टोर द्वारा प्रबंधित प्लगइन सेट करने में असमर्थ।" + }, + { + "id": "app.plugin.filesystem.app_error", + "translation": "फाइल सिस्टम त्रुटि का सामना करना पड़ा।" + }, + { + "id": "app.plugin.extract.app_error", + "translation": "प्लगइन बंडल निकालने में एक त्रुटि हुई।" + }, + { + "id": "app.plugin.disabled.app_error", + "translation": "प्लगइन्स अक्षम कर दिए गए हैं। विवरण के लिए कृपया अपने लॉग जांचें।" + }, + { + "id": "app.plugin.delete_public_key.delete.app_error", + "translation": "सार्वजनिक कुंजी को हटाते समय कोई त्रुटि उत्पन्न हुई।" + }, + { + "id": "app.plugin.deactivate.app_error", + "translation": "प्लगइन निष्क्रिय करने में असमर्थ।" + }, + { + "id": "app.plugin.config.app_error", + "translation": "config में प्लगइन स्थिति सहेजने में त्रुटि।" + }, + { + "id": "app.plugin.cluster.save_config.app_error", + "translation": "क्लस्टरिंग सक्षम के साथ ReadOnlyConfig का उपयोग करते समय आपकी config.json फ़ाइल में प्लगइन कॉन्फ़िगरेशन को मैन्युअल रूप से अपडेट किया जाना चाहिए।" + }, + { + "id": "app.oauth.update_app.updating.app_error", + "translation": "हमें ऐप को अपडेट करने में एक त्रुटि का सामना करना पड़ा।" + }, + { + "id": "app.oauth.update_app.find.app_error", + "translation": "अद्यतन करने के लिए मौजूदा ऐप को खोजने में असमर्थ।" + }, + { + "id": "app.oauth.save_app.save.app_error", + "translation": "ऐप्लिकेशन सहेजने में असमर्थ।" + }, + { + "id": "app.oauth.save_app.existing.app_error", + "translation": "मौजूदा ऐप के लिए अपडेट कॉल करना होगा।" + }, + { + "id": "app.oauth.remove_access_data.app_error", + "translation": "पहुंच टोकन को निकालने में असमर्थ।" + }, + { + "id": "app.notification.subject.group_message.full", + "translation": "[{{ .SiteName }}] {{.ChannelName}} में {{.Month}} {{.Day}}, {{.Year}} को नया ग्रुप मैसेज" + }, + { + "id": "app.notification.subject.direct.full", + "translation": "[{{.SiteName}}] {{.SenderDisplayName}} की ओर से {{.Month}} {{.Day}}, {{.Year}} को नया सीधा संदेश" + }, + { + "id": "api.user.authorize_oauth_user.saml_response_too_long.app_error", + "translation": "SAML प्रतिक्रिया बहुत लंबी है" + }, + { + "id": "api.team.update_restricted_domains.mismatch.app_error", + "translation": "सिस्टम कॉन्फिग द्वारा टीम को {{ .Domain }} तक प्रतिबंधित करने की अनुमति नहीं है। कृपया अपने सिस्टम व्यवस्थापक से संपर्क करें।" + }, + { + "id": "api.slackimport.slack_add_users.merge_existing_failed", + "translation": "मेल खाने वाले ईमेल {{.Email}} और उपयोगकर्ता नाम {{.Username}} के साथ स्लैक उपयोगकर्ता का मौजूदा मैटरमोस्ट उपयोगकर्ता के साथ विलय हो गया, लेकिन वह उपयोगकर्ता को अपनी टीम में जोड़ने में असमर्थ था।\n" + }, + { + "id": "api.server.warn_metric.number_of_posts_2M.start_trial.notification_success.message", + "translation": "आपका एंटरप्राइज़ परीक्षण अब सक्रिय है। एक बार आपके पास इलास्टिक्स खोज सर्वर होने के बाद, इलास्टिक्स खोज को कॉन्फ़िगर करने के लिए **System Console > Environment > Elasticsearch** पर जाएं।" + }, + { + "id": "api.server.warn_metric.number_of_active_users_200.start_trial.notification_success.message", + "translation": "आपका एंटरप्राइज़ परीक्षण अब सक्रिय है। अपने SAML 2.0 प्रदाता के साथ एकीकृत करने के लिए **सिस्टम कंसोल> प्रमाणीकरण> SAML 2.0** पर जाएं।" + }, + { + "id": "api.server.warn_metric.number_of_active_users_100.start_trial.notification_success.message", + "translation": "आपका एंटरप्राइज़ परीक्षण अब सक्रिय है। अपनी AD/LDAP सेवा को एकीकृत करने के लिए **सिस्टम कंसोल> प्रमाणीकरण> AD/LDAP** पर जाएं।" + }, + { + "id": "api.server.warn_metric.mfa.start_trial_notification_success.message", + "translation": "आपका एंटरप्राइज़ परीक्षण अब सक्रिय है। बहु-कारक प्रमाणीकरण लागू करने के लिए **System Console > Authentication > MFA** पर जाएं।" + }, + { + "id": "api.server.warn_metric.email_domain.notification_body", + "translation": "परियोजनाओं में अक्सर संगठन के अंदर और बाहर दोनों जगह के लोग शामिल होते हैं। अतिथि खातों के साथ, आप बाहरी भागीदारों को अपने मैटरमोस्ट सिस्टम में ला सकते हैं और निर्दिष्ट कर सकते हैं कि वे किसके साथ काम कर सकते हैं और वे क्या देख सकते हैं।\n\n[अतिथि खातों को सक्षम करने के बारे में अधिक जानें](https://www.mattermost.com/docs-guest-accounts/?utm_medium=product&utm_source=mattermost-advisor-bot&utm_content=guest-accounts)।\n\nहमसे संपर्क करें क्लिक करके, आप मैटरमोस्ट, इंक. के साथ अपनी जानकारी साझा करेंगे। [अधिक जानें](https://mattermost.com/pl/default-admin-advisory)" + }, + { + "id": "api.post.get_message_for_notification.images_sent", + "translation": { + "one": "{{.Count}} छवि भेजी गई: {{.Filenames}}", + "other": "{{.Count}} छवियां भेजी गईं: {{.Filenames}}" + } + }, + { + "id": "api.error_get_first_admin_complete_setup", + "translation": "स्टोर से पहला व्यवस्थापक पूर्ण सेटअप पुनर्प्राप्त करने का प्रयास करने में त्रुटि।" + }, + { + "id": "model.config.is_valid.saml_public_cert.app_error", + "translation": "सर्विस प्रोवाइडर पब्लिक सर्टिफिकेट गायब है। क्या आप इसे अपलोड करना भूल गए?" + }, + { + "id": "model.config.is_valid.saml_idp_url.app_error", + "translation": "एसएएमएल एसएसओ यूआरएल एक मान्य यूआरएल होना चाहिए और http:// या https:// से शुरू होना चाहिए।" + }, + { + "id": "model.config.is_valid.saml_idp_cert.app_error", + "translation": "पहचान प्रदाता सार्वजनिक प्रमाणपत्र गुम है। क्या आप इसे अपलोड करना भूल गए?" + }, + { + "id": "model.config.is_valid.saml_guest_attribute.app_error", + "translation": "अमान्य अतिथि विशेषता। 'field=value' के रूप में होना चाहिए।" + }, + { + "id": "model.config.is_valid.saml_canonical_algorithm.app_error", + "translation": "अमान्य विहित एल्गोरिथम।" + }, + { + "id": "model.config.is_valid.saml_assertion_consumer_service_url.app_error", + "translation": "सेवा प्रदाता लॉगिन यूआरएल एक वैध यूआरएल होना चाहिए और http:// या https:// से शुरू होना चाहिए।" + }, + { + "id": "model.config.is_valid.saml_admin_attribute.app_error", + "translation": "अमान्य व्यवस्थापक विशेषता। 'field=value' के रूप में होना चाहिए।" + }, + { + "id": "model.config.is_valid.restrict_direct_message.app_error", + "translation": "अमान्य प्रत्यक्ष संदेश प्रतिबंध। 'कोई', या 'टीम' होना चाहिए।" + }, + { + "id": "model.config.is_valid.read_timeout.app_error", + "translation": "पठन टाइमआउट के लिए अमान्य मान।" + }, + { + "id": "model.config.is_valid.rate_sec.app_error", + "translation": "दर सीमा सेटिंग के लिए अमान्य प्रति सेकंड। अवश्य ही एक धनात्मक संख्या होनी चाहिए।" + }, + { + "id": "model.config.is_valid.rate_mem.app_error", + "translation": "दर सीमा सेटिंग्स के लिए अमान्य मेमोरी स्टोर आकार। अवश्य ही एक धनात्मक संख्या होनी चाहिए।" + }, + { + "id": "model.config.is_valid.password_length.app_error", + "translation": "न्यूनतम पासवर्ड लंबाई {{.MinLength}} से अधिक या उसके बराबर और {{.MaxLength}} से कम या उसके बराबर होनी चाहिए।" + }, + { + "id": "model.config.is_valid.message_export.global_relay.smtp_username.app_error", + "translation": "संदेश निर्यात कार्य GlobalRelaySettings.SmtpUsername सेट होना चाहिए।" + }, + { + "id": "model.config.is_valid.message_export.global_relay.smtp_password.app_error", + "translation": "संदेश निर्यात कार्य GlobalRelaySettings.SmtpPassword अवश्य सेट होना चाहिए।" + }, + { + "id": "model.config.is_valid.message_export.global_relay.email_address.app_error", + "translation": "संदेश निर्यात कार्य GlobalRelaySettings.EmailAddress को एक मान्य ईमेल पते पर सेट किया जाना चाहिए।" + }, + { + "id": "model.config.is_valid.message_export.global_relay.customer_type.app_error", + "translation": "संदेश निर्यात GlobalRelaySettings.CustomerType को 'A9' या 'A10' में से किसी एक पर सेट किया जाना चाहिए।" + }, + { + "id": "model.config.is_valid.message_export.global_relay.config_missing.app_error", + "translation": "संदेश निर्यात कार्य ExportFormat 'globalrelay' पर सेट है, लेकिन GlobalRelaySettings अनुपलब्ध हैं।" + }, + { + "id": "model.config.is_valid.message_export.export_type.app_error", + "translation": "संदेश निर्यात कार्य ExportFormat 'क्रिया', 'csv' या 'globalrelay' में से एक होना चाहिए।" + }, + { + "id": "model.config.is_valid.message_export.export_from.app_error", + "translation": "संदेश निर्यात कार्य ExportFromTimestamp एक टाइमस्टैम्प होना चाहिए (यूनिक्स युग के बाद से सेकंड में व्यक्त)। केवल इस टाइमस्टैम्प के बाद भेजे गए संदेशों को निर्यात किया जाएगा।" + }, + { + "id": "model.config.is_valid.message_export.enable.app_error", + "translation": "संदेश निर्यात कार्य EnableExport सेटिंग या तो सही या गलत होनी चाहिए।" + }, + { + "id": "model.config.is_valid.message_export.daily_runtime.app_error", + "translation": "संदेश निर्यात कार्य DailyRuntime HH:MM के रूप में 24-घंटे का टाइम स्टैम्प होना चाहिए।" + }, + { + "id": "model.config.is_valid.message_export.batch_size.app_error", + "translation": "संदेश निर्यात कार्य बैच आकार एक धनात्मक पूर्णांक होना चाहिए।" + }, + { + "id": "model.config.is_valid.max_users.app_error", + "translation": "टीम सेटिंग के लिए प्रति टीम अमान्य अधिकतम उपयोगकर्ता। अवश्य ही एक धनात्मक संख्या होनी चाहिए।" + }, + { + "id": "model.config.is_valid.max_file_size.app_error", + "translation": "फ़ाइल सेटिंग के लिए अमान्य अधिकतम फ़ाइल आकार। शून्य से बड़ी एक पूर्ण संख्या होनी चाहिए।" + }, + { + "id": "model.config.is_valid.max_burst.app_error", + "translation": "अधिकतम बर्स्ट आकार शून्य से बड़ा होना चाहिए।" + }, + { + "id": "model.config.is_valid.login_attempts.app_error", + "translation": "सेवा सेटिंग्स के लिए अमान्य अधिकतम लॉगिन प्रयास। अवश्य ही एक धनात्मक संख्या होनी चाहिए।" + }, + { + "id": "model.config.is_valid.localization.available_locales.app_error", + "translation": "उपलब्ध भाषाओं में डिफ़ॉल्ट क्लाइंट भाषा होनी चाहिए।" + }, + { + "id": "model.config.is_valid.ldap_sync_interval.app_error", + "translation": "अमान्य सिंक अंतराल समय। कम से कम एक मिनट होना चाहिए।" + }, + { + "id": "model.config.is_valid.ldap_server", + "translation": "AD/LDAP फ़ील्ड \"AD/LDAP सर्वर\" आवश्यक है।" + }, + { + "id": "model.config.is_valid.ldap_max_page_size.app_error", + "translation": "अमान्य अधिकतम पृष्ठ आकार मान." + }, + { + "id": "model.config.is_valid.ldap_login_id", + "translation": "AD/LDAP फ़ील्ड \"लॉगिन आईडी विशेषता\" आवश्यक है।" + }, + { + "id": "model.config.is_valid.ldap_id", + "translation": "AD/LDAP फ़ील्ड \"आईडी विशेषता\" आवश्यक है।" + }, + { + "id": "model.config.is_valid.ldap_email", + "translation": "AD/LDAP फ़ील्ड \"ईमेल विशेषता\" आवश्यक है।" + }, + { + "id": "model.config.is_valid.ldap_basedn", + "translation": "AD/LDAP फ़ील्ड \"बेसडीएन\" आवश्यक है।" + }, + { + "id": "model.config.is_valid.image_proxy_type.app_error", + "translation": "अमान्य छवि प्रॉक्सी प्रकार. 'local' या 'atmos/camo' होना चाहिए।" + }, + { + "id": "model.config.is_valid.group_unread_channels.app_error", + "translation": "सेवा सेटिंग के लिए अमान्य समूह अपठित चैनल. 'अक्षम', 'डिफ़ॉल्ट_ऑन', या 'डिफ़ॉल्ट_ऑफ़' होना चाहिए।" + }, + { + "id": "model.config.is_valid.file_salt.app_error", + "translation": "फ़ाइल सेटिंग के लिए अमान्य सार्वजनिक लिंक नमक. 32 वर्ण या अधिक होना चाहिए।" + }, + { + "id": "model.config.is_valid.encrypt_sql.app_error", + "translation": "SQL सेटिंग्स के लिए रेस्ट एनक्रिप्ट कुंजी पर अमान्य। 32 वर्ण या अधिक होना चाहिए।" + }, + { + "id": "model.config.is_valid.email_security.app_error", + "translation": "ईमेल सेटिंग के लिए अमान्य कनेक्शन सुरक्षा. '', 'TLS', या 'STARTTLS' होना चाहिए।" + }, + { + "id": "model.config.is_valid.email_notification_contents_type.app_error", + "translation": "ईमेल सेटिंग के लिए अमान्य ईमेल सूचना सामग्री प्रकार। या तो 'पूर्ण' या 'सामान्य' में से एक होना चाहिए।" + }, + { + "id": "model.config.is_valid.email_batching_interval.app_error", + "translation": "ईमेल सेटिंग के लिए अमान्य ईमेल बैचिंग अंतराल. 30 सेकंड या उससे अधिक का होना चाहिए।" + }, + { + "id": "model.config.is_valid.email_batching_buffer_size.app_error", + "translation": "ईमेल सेटिंग के लिए अमान्य ईमेल बैचिंग बफ़र आकार. शून्य या धनात्मक संख्या होनी चाहिए।" + }, + { + "id": "model.config.is_valid.elastic_search.request_timeout_seconds.app_error", + "translation": "इलास्टिक्स खोज अनुरोध टाइमआउट कम से कम 1 सेकंड होना चाहिए।" + }, + { + "id": "model.config.is_valid.elastic_search.posts_aggregator_job_start_time.app_error", + "translation": "Elasticsearch PostsAggregatorJobStartTime सेटिंग \"hh:mm\" प्रारूप में एक समय होनी चाहिए।" + }, + { + "id": "model.config.is_valid.elastic_search.live_indexing_batch_size.app_error", + "translation": "Elasticsearch लाइव इंडेक्सिंग बैच का आकार कम से कम 1 होना चाहिए।" + }, + { + "id": "model.config.is_valid.elastic_search.enable_searching.app_error", + "translation": "जब Elasticsearch EnableSearching को सही पर सेट किया जाता है, तो Elasticsearch EnableIndexing सेटिंग को सही पर सेट किया जाना चाहिए" + }, + { + "id": "model.config.is_valid.elastic_search.enable_autocomplete.app_error", + "translation": "Elasticsearch EnableIndexing सेटिंग को सत्य पर सेट किया जाना चाहिए जब Elasticsearch सक्षम स्वत: पूर्ण सत्य पर सेट हो" + }, + { + "id": "model.config.is_valid.directory.app_error", + "translation": "अमान्य स्थानीय संग्रहण निर्देशिका। एक गैर-रिक्त स्ट्रिंग होना चाहिए।" + }, + { + "id": "model.config.is_valid.data_retention.message_retention_days_too_low.app_error", + "translation": "संदेश प्रतिधारण एक दिन या उससे अधिक समय का होना चाहिए।" + }, + { + "id": "model.config.is_valid.data_retention.file_retention_days_too_low.app_error", + "translation": "फ़ाइल प्रतिधारण एक दिन या उससे अधिक समय तक होना चाहिए।" + }, + { + "id": "model.config.is_valid.data_retention.deletion_job_start_time.app_error", + "translation": "डेटा प्रतिधारण कार्य प्रारंभ समय HH:MM के रूप में 24-घंटे का टाइम स्टैम्प होना चाहिए।" + }, + { + "id": "model.config.is_valid.cluster_email_batching.app_error", + "translation": "क्लस्टरिंग सक्षम होने पर ईमेल बैचिंग सक्षम करने में असमर्थ।" + }, + { + "id": "model.config.is_valid.bleve_search.filename.app_error", + "translation": "Bleve IndexingDir सेटिंग तब सेट की जानी चाहिए जब Bleve EnableIndexing सही पर सेट हो" + }, + { + "id": "model.config.is_valid.bleve_search.enable_searching.app_error", + "translation": "जब Bleve EnableSearching को सही पर सेट किया जाता है, तो Bleve EnableIndexing सेटिंग को सही पर सेट किया जाना चाहिए" + }, + { + "id": "model.config.is_valid.bleve_search.enable_autocomplete.app_error", + "translation": "जब Bleve EnableAutocomplete को सही पर सेट किया जाता है, तो Bleve EnableIndexing सेटिंग को सही पर सेट किया जाना चाहिए" + }, + { + "id": "model.config.is_valid.atmos_camo_image_proxy_url.app_error", + "translation": "Atmos/camo के लिए अमान्य RemoteImageProxyURL. आपकी साझा कुंजी पर सेट होना चाहिए।" + }, + { + "id": "model.config.is_valid.atmos_camo_image_proxy_options.app_error", + "translation": "atmos/camo के लिए अमान्य RemoteImageProxyOptions। आपकी साझा कुंजी पर सेट होना चाहिए।" + }, + { + "id": "model.config.is_valid.allow_cookies_for_subdomains.app_error", + "translation": "उप डोमेन के लिए कुकीज़ की अनुमति देने के लिए SiteURL को सेट करना आवश्यक है।" + }, + { + "id": "model.compliance.is_valid.start_end_at.app_error", + "translation": "To बड़ा होना चाहिए From से।" + }, + { + "id": "model.compliance.is_valid.start_at.app_error", + "translation": "से एक मान्य समय होना चाहिए।" + }, + { + "id": "model.compliance.is_valid.end_at.app_error", + "translation": "के लिए एक वैध समय होना चाहिए।" + }, + { + "id": "model.link_metadata.is_valid.data_type.app_error", + "translation": "लिंक मेटाडेटा डेटा दिए गए प्रकार से मेल नहीं खाता।" + }, + { + "id": "model.link_metadata.is_valid.data.app_error", + "translation": "लिंक मेटाडेटा डेटा शून्य नहीं हो सकता।" + }, + { + "id": "model.license_record.is_valid.id.app_error", + "translation": "लाइसेंस अपलोड करते समय आईडी के लिए अमान्य मान।" + }, + { + "id": "model.license_record.is_valid.create_at.app_error", + "translation": "लाइसेंस अपलोड करते समय create_at के लिए अमान्य मान।" + }, + { + "id": "model.job.is_valid.type.app_error", + "translation": "अमान्य कार्य प्रकार।" + }, + { + "id": "model.job.is_valid.status.app_error", + "translation": "अमान्य कार्य स्थिति।" + }, + { + "id": "model.job.is_valid.id.app_error", + "translation": "अमान्य कार्य Id।" + }, + { + "id": "model.job.is_valid.create_at.app_error", + "translation": "पर बनाएं एक मान्य समय होना चाहिए।" + }, + { + "id": "model.incoming_hook.username.app_error", + "translation": "अमान्य उपयोगकर्ता नाम।" + }, + { + "id": "model.incoming_hook.user_id.app_error", + "translation": "अमान्य उपयोगकर्ता आइ डी।" + }, + { + "id": "model.incoming_hook.update_at.app_error", + "translation": "पर अद्यतन एक मान्य समय होना चाहिए।" + }, + { + "id": "model.incoming_hook.team_id.app_error", + "translation": "अमान्य टीम आईडी।" + }, + { + "id": "model.incoming_hook.parse_data.app_error", + "translation": "आने वाले डेटा को पार्स करने में असमर्थ।" + }, + { + "id": "model.incoming_hook.id.app_error", + "translation": "अमान्य पहचान पत्र।" + }, + { + "id": "model.incoming_hook.icon_url.app_error", + "translation": "अमान्य पोस्ट आइकन।" + }, + { + "id": "model.incoming_hook.display_name.app_error", + "translation": "अमान्य शीर्षक।" + }, + { + "id": "model.incoming_hook.description.app_error", + "translation": "अमान्य विवरण।" + }, + { + "id": "model.incoming_hook.create_at.app_error", + "translation": "पर बनाएं एक मान्य समय होना चाहिए।" + }, + { + "id": "model.incoming_hook.channel_id.app_error", + "translation": "अमान्य चैनल आईडी।" + }, + { + "id": "model.guest.is_valid.email.app_error", + "translation": "अमान्य ईमेल।" + }, + { + "id": "model.guest.is_valid.channel.app_error", + "translation": "अमान्य चैनल।" + }, + { + "id": "model.group_syncable.syncable_id.app_error", + "translation": "समूह syncable करने योग्य के लिए अमान्य syncable करने योग्य आईडी।" + }, + { + "id": "model.group_syncable.group_id.app_error", + "translation": "समूह syncable करने योग्य के लिए अमान्य समूह आईडी संपत्ति।" + }, + { + "id": "model.group_member.user_id.app_error", + "translation": "समूह सदस्य के लिए अमान्य उपयोगकर्ता आईडी संपत्ति।" + }, + { + "id": "model.group_member.group_id.app_error", + "translation": "समूह सदस्य के लिए अमान्य समूह आईडी संपत्ति।" + }, + { + "id": "model.group.update_at.app_error", + "translation": "समूह के लिए संपत्ति पर अमान्य अद्यतन।" + }, + { + "id": "model.group.source.app_error", + "translation": "समूह के लिए अमान्य स्रोत संपत्ति।" + }, + { + "id": "model.group.remote_id.app_error", + "translation": "समूह के लिए अमान्य दूरस्थ आईडी संपत्ति।" + }, + { + "id": "model.group.name.invalid_chars.app_error", + "translation": "समूह के लिए नाम संपत्ति में अमान्य वर्ण" + }, + { + "id": "model.group.name.app_error", + "translation": "समूह के लिए अमान्य नाम संपत्ति।" + }, + { + "id": "model.group.display_name.app_error", + "translation": "समूह के लिए अमान्य प्रदर्शन नाम संपत्ति।" + }, + { + "id": "model.group.description.app_error", + "translation": "समूह के लिए अमान्य विवरण संपत्ति।" + }, + { + "id": "model.group.create_at.app_error", + "translation": "समूह के लिए संपत्ति पर अवैध निर्माण।" + }, + { + "id": "model.emoji.update_at.app_error", + "translation": "पर अद्यतन एक मान्य समय होना चाहिए।" + }, + { + "id": "model.emoji.system_emoji_name.app_error", + "translation": "नाम मौजूदा सिस्टम इमोजी नाम के साथ विरोध करता है।" + }, + { + "id": "model.emoji.create_at.app_error", + "translation": "पर बनाएं एक मान्य समय होना चाहिए।" + }, + { + "id": "model.config.is_valid.write_timeout.app_error", + "translation": "टाइमआउट लिखने के लिए अमान्य मान।" + }, + { + "id": "app.system.get_onboarding_request.app_error", + "translation": "ऑनबोर्डिंग पूर्णता स्थिति प्राप्त करने में विफल।" + }, + { + "id": "app.system.get_by_name.app_error", + "translation": "सिस्टम चर खोजने में असमर्थ।" + }, + { + "id": "app.system.get.app_error", + "translation": "हमें सिस्टम गुण खोजने में त्रुटि का सामना करना पड़ा।" + }, + { + "id": "app.system.complete_onboarding_request.no_first_user", + "translation": "ऑनबोर्डिंग केवल एक सिस्टम प्रशासक द्वारा पूरा किया जा सकता है।" + }, + { + "id": "app.system.complete_onboarding_request.app_error", + "translation": "संपूर्ण ऑनबोर्डिंग अनुरोध को डीकोड करने में विफल।" + }, + { + "id": "app.submit_interactive_dialog.json_error", + "translation": "इंटरैक्टिव संवाद के लिए JSON को एन्कोड करने में त्रुटि हुई।" + }, + { + "id": "app.status.get.missing.app_error", + "translation": "उस स्थिति के लिए कोई प्रविष्टि मौजूद नहीं है।" + }, + { + "id": "app.status.get.app_error", + "translation": "स्थिति प्राप्त करने में त्रुटि आई।" + }, + { + "id": "app.sharedchannel.dm_channel_creation.internal_error", + "translation": "एक सीधा साझा चैनल बनाते समय एक त्रुटि का सामना करना पड़ा।" + }, + { + "id": "app.session.update_device_id.app_error", + "translation": "डिवाइस आईडी अपडेट करने में असमर्थ।" + }, + { + "id": "app.session.save.existing.app_error", + "translation": "मौजूदा सत्र को अपडेट करने में असमर्थ।" + }, + { + "id": "app.session.save.app_error", + "translation": "सत्र सहेजने में असमर्थ।" + }, + { + "id": "app.session.remove_all_sessions_for_team.app_error", + "translation": "सभी सत्र निकालने में असमर्थ।" + }, + { + "id": "app.session.remove.app_error", + "translation": "सत्र निकालने में असमर्थ।" + }, + { + "id": "app.session.permanent_delete_sessions_by_user.app_error", + "translation": "उपयोगकर्ता के लिए सभी सत्र निकालने में असमर्थ।" + }, + { + "id": "app.session.get_sessions.app_error", + "translation": "उपयोगकर्ता सत्र ढूंढते समय हमें एक त्रुटि का सामना करना पड़ा।" + }, + { + "id": "app.session.get.app_error", + "translation": "हमें सत्र खोजने में एक त्रुटि का सामना करना पड़ा।" + }, + { + "id": "app.session.extend_session_expiry.app_error", + "translation": "सत्र की लंबाई बढ़ाने में असमर्थ" + }, + { + "id": "app.session.analytics_session_count.app_error", + "translation": "सत्रों की गणना करने में असमर्थ।" + }, + { + "id": "app.role.get_all.app_error", + "translation": "सारे रोल नहीं मिल पा रहे हैं।" + }, + { + "id": "app.reaction.save.save.app_error", + "translation": "प्रतिक्रिया सहेजने में असमर्थ।" + }, + { + "id": "app.preference.save.updating.app_error", + "translation": "प्राथमिकताएं अपडेट करते समय हमें एक त्रुटि का सामना करना पड़ा।" + }, + { + "id": "app.preference.permanent_delete_by_user.app_error", + "translation": "वरीयताएँ हटाते समय हमें एक त्रुटि का सामना करना पड़ा।" + }, + { + "id": "app.preference.get_category.app_error", + "translation": "प्राथमिकताएं ढूंढते समय हमें एक त्रुटि का सामना करना पड़ा।" + }, + { + "id": "app.preference.get_all.app_error", + "translation": "प्राथमिकताएं ढूंढते समय हमें एक त्रुटि का सामना करना पड़ा।" + }, + { + "id": "app.preference.get.app_error", + "translation": "प्राथमिकताएं ढूंढते समय हमें एक त्रुटि का सामना करना पड़ा।" + }, + { + "id": "app.preference.delete.app_error", + "translation": "वरीयताएँ हटाते समय हमें एक त्रुटि का सामना करना पड़ा।" + }, + { + "id": "app.post.permanent_delete_by_user.app_error", + "translation": "उपयोगकर्ता के लिए हटाने के लिए पदों का चयन करने में असमर्थ।" + }, + { + "id": "app.post.overwrite.app_error", + "translation": "पोस्ट को अधिलेखित करने में असमर्थ।" + }, + { + "id": "app.post.get_posts_batch_for_indexing.get.app_error", + "translation": "अनुक्रमण के लिए पोस्ट बैच प्राप्त करने में असमर्थ।" + }, + { + "id": "app.post.get_posts.app_error", + "translation": "पेजिंग के लिए सीमा पार हो गई है।" + }, + { + "id": "app.post.get_post_id_around.app_error", + "translation": "समय से पोस्ट नहीं मिल पा रहा है।" + }, + { + "id": "app.post.get_post_after_time.app_error", + "translation": "समय सीमा के बाद पोस्ट प्राप्त करने में असमर्थ।" + }, + { + "id": "app.post.get_flagged_posts.app_error", + "translation": "फ़्लैग किए गए पोस्ट प्राप्त करने में असमर्थ।" + }, + { + "id": "app.post.get_direct_posts.app_error", + "translation": "सीधे पोस्ट प्राप्त करने में असमर्थ।" + }, + { + "id": "app.post.get.app_error", + "translation": "पोस्ट नहीं मिल पा रहा है।" + }, + { + "id": "app.post.delete.app_error", + "translation": "पोस्ट को हटाने में असमर्थ।" + }, + { + "id": "app.post.analytics_user_counts_posts_by_day.app_error", + "translation": "पोस्ट के साथ उपयोगकर्ता संख्या प्राप्त करने में असमर्थ।" + }, + { + "id": "app.post.analytics_posts_count_by_day.app_error", + "translation": "दिन के हिसाब से पोस्ट की संख्या प्राप्त करने में असमर्थ।" + }, + { + "id": "app.post.analytics_posts_count.app_error", + "translation": "पद संख्या प्राप्त करने में असमर्थ।" + }, + { + "id": "app.plugin_store.save.app_error", + "translation": "प्लगइन कुंजी मान को सहेज या अपडेट नहीं कर सका।" + }, + { + "id": "app.plugin_store.list.app_error", + "translation": "सभी प्लगइन कुंजियों को सूचीबद्ध करने में असमर्थ।" + }, + { + "id": "app.plugin_store.get.app_error", + "translation": "प्लगइन कुंजी मान नहीं मिल सका।" + }, + { + "id": "app.plugin_store.delete.app_error", + "translation": "प्लग इन कुंजी मान मिटाया नहीं जा सका।" + }, + { + "id": "app.plugin.write_file.saving.app_error", + "translation": "फ़ाइल सहेजते समय एक त्रुटि हुई।" + }, + { + "id": "app.plugin.write_file.read.app_error", + "translation": "फ़ाइल पढ़ते समय एक त्रुटि हुई।" + }, + { + "id": "app.plugin.webapp_bundle.app_error", + "translation": "प्लगइन वेबएप बंडल जनरेट करने में असमर्थ।" + }, + { + "id": "app.plugin.upload_disabled.app_error", + "translation": "प्लगइन्स और/या प्लगइन अपलोड अक्षम कर दिए गए हैं।" + }, + { + "id": "app.plugin.sync.read_local_folder.app_error", + "translation": "स्थानीय प्लगइन्स फ़ोल्डर पढ़ने में त्रुटि।" + }, + { + "id": "app.plugin.sync.list_filestore.app_error", + "translation": "फ़ाइल संग्रह में प्लग इन फ़ोल्डर से फ़ाइलें पढ़ने में त्रुटि." + }, + { + "id": "app.plugin.store_signature.app_error", + "translation": "कॉन्फ़िगर की गई फ़ाइल संग्रह में प्लग इन हस्ताक्षर संगृहीत करने में असमर्थ." + }, + { + "id": "app.plugin.store_bundle.app_error", + "translation": "प्लग इन को कॉन्फ़िगर की गई फ़ाइल स्टोर में संग्रहीत करने में असमर्थ।" + }, + { + "id": "app.plugin.signature_decode.app_error", + "translation": "बेस 64 हस्ताक्षर को डीकोड करने में असमर्थ।" + }, + { + "id": "app.plugin.restart.app_error", + "translation": "अपग्रेड पर प्लग इन को पुनरारंभ करने में असमर्थ।" + }, + { + "id": "app.plugin.remove_bundle.app_error", + "translation": "फ़ाइल स्टोर से प्लगइन बंडल को निकालने में असमर्थ।" + }, + { + "id": "app.plugin.remove.app_error", + "translation": "प्लगइन हटाने में असमर्थ।" + }, + { + "id": "app.plugin.not_installed.app_error", + "translation": "प्लगइन स्थापित नहीं है।" + }, + { + "id": "app.plugin.mvdir.app_error", + "translation": "प्लगइन को अस्थायी निर्देशिका से अंतिम गंतव्य तक ले जाने में असमर्थ। एक अन्य प्लगइन समान निर्देशिका नाम का उपयोग कर सकता है।" + }, + { + "id": "app.plugin.modify_saml.app_error", + "translation": "saml फ़ाइलों को संशोधित नहीं कर सकता।" + }, + { + "id": "app.plugin.marshal.app_error", + "translation": "मार्केटप्लेस प्लगइन्स को मार्शल करने में विफल।" + }, + { + "id": "app.plugin.marketplace_plugins.signature_not_found.app_error", + "translation": "अनुरोधित मार्केटप्लेस प्लगइन हस्ताक्षर नहीं मिल सका।" + }, + { + "id": "app.plugin.marketplace_plugins.not_found.app_error", + "translation": "अनुरोधित मार्केटप्लेस प्लगइन नहीं मिला।" + }, + { + "id": "app.plugin.marketplace_plugin_request.app_error", + "translation": "मार्केटप्लेस प्लगइन अनुरोध को डीकोड करने में विफल।" + }, + { + "id": "model.user_access_token.is_valid.token.app_error", + "translation": "अमान्य प्रवेश टोकन।" + }, + { + "id": "model.user.is_valid.update_at.app_error", + "translation": "पर अद्यतन एक मान्य समय होना चाहिए।" + }, + { + "id": "model.user.is_valid.locale.app_error", + "translation": "अमान्य स्थान।" + }, + { + "id": "model.user.is_valid.create_at.app_error", + "translation": "पर बनाएं एक मान्य समय होना चाहिए।" + }, + { + "id": "model.user.is_valid.auth_data_type.app_error", + "translation": "अमान्य उपयोगकर्ता, प्रमाणन डेटा को प्रमाणन प्रकार के साथ सेट किया जाना चाहिए।" + }, + { + "id": "model.team.is_valid.update_at.app_error", + "translation": "पर अद्यतन एक मान्य समय होना चाहिए।" + }, + { + "id": "model.team.is_valid.create_at.app_error", + "translation": "पर बनाएं एक मान्य समय होना चाहिए।" + }, + { + "id": "model.session.is_valid.user_id.app_error", + "translation": "सत्र के लिए अमान्य UserId फ़ील्ड।" + }, + { + "id": "model.session.is_valid.id.app_error", + "translation": "सत्र के लिए अमान्य आईडी फ़ील्ड।" + }, + { + "id": "model.session.is_valid.create_at.app_error", + "translation": "सत्र के लिए अमान्य CreateAt फ़ील्ड।" + }, + { + "id": "model.search_params_list.is_valid.include_deleted_channels.app_error", + "translation": "सभी शामिल हटाए गए चैनल पैरा का एक ही मान होना चाहिए।" + }, + { + "id": "model.reaction.is_valid.create_at.app_error", + "translation": "पर बनाएं एक मान्य समय होना चाहिए।" + }, + { + "id": "model.post.is_valid.update_at.app_error", + "translation": "पर अद्यतन एक मान्य समय होना चाहिए।" + }, + { + "id": "model.post.is_valid.create_at.app_error", + "translation": "पर बनाएं एक मान्य समय होना चाहिए।" + }, + { + "id": "model.outgoing_hook.is_valid.update_at.app_error", + "translation": "पर अद्यतन एक मान्य समय होना चाहिए।" + }, + { + "id": "model.outgoing_hook.is_valid.create_at.app_error", + "translation": "पर बनाएं एक मान्य समय होना चाहिए।" + }, + { + "id": "model.oauth.is_valid.update_at.app_error", + "translation": "पर अद्यतन एक मान्य समय होना चाहिए।" + }, + { + "id": "model.oauth.is_valid.description.app_error", + "translation": "अमान्य विवरण।" + }, + { + "id": "model.oauth.is_valid.creator_id.app_error", + "translation": "अमान्य क्रिएटर आईडी।" + }, + { + "id": "model.oauth.is_valid.create_at.app_error", + "translation": "पर बनाएं एक मान्य समय होना चाहिए।" + }, + { + "id": "model.oauth.is_valid.client_secret.app_error", + "translation": "अमान्य ग्राहक रहस्य।" + }, + { + "id": "model.oauth.is_valid.callback.app_error", + "translation": "कॉलबैक यूआरएल एक वैध यूआरएल होना चाहिए और http:// या https:// से शुरू होना चाहिए।" + }, + { + "id": "model.oauth.is_valid.app_id.app_error", + "translation": "अमान्य ऐप्लिकेशन आईडी।" + }, + { + "id": "model.link_metadata.is_valid.url.app_error", + "translation": "लिंक मेटाडेटा URL सेट होना चाहिए।" + }, + { + "id": "model.link_metadata.is_valid.type.app_error", + "translation": "अमान्य लिंक मेटाडेटा प्रकार।" + }, + { + "id": "model.link_metadata.is_valid.timestamp.app_error", + "translation": "लिंक मेटाडेटा टाइमस्टैम्प गैर-शून्य होना चाहिए और निकटतम घंटे तक गोल होना चाहिए।" + }, + { + "id": "Boards", + "translation": "बोर्डों" + }, + { + "id": "web.error.unsupported_browser.browser_title.edge", + "translation": "माइक्रोसॉफ्ट एज" + }, + { + "id": "searchengine.bleve.disabled.error", + "translation": "Bleve अनुक्रमणिका को शुद्ध करने में त्रुटि: इंजन अक्षम है" + }, + { + "id": "api.roles.patch_roles.not_allowed_permission.error", + "translation": "निम्न में से एक या अधिक अनुमतियाँ जिन्हें आप जोड़ने या निकालने का प्रयास कर रहे हैं, की अनुमति नहीं है" + }, + { + "id": "api.post.search_files.invalid_body.app_error", + "translation": "अनुरोध के मुख्य भाग को पार्स करने में असमर्थ।" + }, + { + "id": "api.oauth.redirecting_back", + "translation": "आपको ऐप पर वापस रीडायरेक्ट कर रहा है।" + }, + { + "id": "api.oauth.close_browser", + "translation": "अब आप इस ब्राउज़र टैब को बंद कर सकते हैं।" + }, + { + "id": "api.oauth.auth_complete", + "translation": "प्रमाणीकरण पूर्ण" + }, + { + "id": "api.no_license", + "translation": "इस समापन बिंदु का उपयोग करने के लिए E10 या E20 लाइसेंस की आवश्यकता है।" + }, + { + "id": "api.license.request_renewal_link.app_error", + "translation": "लाइसेंस नवीनीकरण लिंक प्राप्त करने में त्रुटि" + }, + { + "id": "api.job.unable_to_download_job.incorrect_job_type", + "translation": "आप जिस कार्य प्रकार को डाउनलोड करने का प्रयास कर रहे हैं वह इस समय समर्थित नहीं है" + }, + { + "id": "api.invalid_redirect_url", + "translation": "अमान्य रीडायरेक्ट url प्रदान किया गया है" + }, + { + "id": "api.invalid_custom_url_scheme", + "translation": "अमान्य कस्टम url योजना प्रदान की गई है" + }, + { + "id": "api.getThreadsForUser.bad_params", + "translation": "GetThreadsForUser के पहले और बाद के पैरामीटर परस्पर अनन्य हैं" + }, + { + "id": "api.file.write_file.app_error", + "translation": "फ़ाइल लिखने में असमर्थ।" + }, + { + "id": "api.file.test_connection.app_error", + "translation": "फ़ाइल संग्रहण तक पहुँचने में असमर्थ।" + }, + { + "id": "api.file.remove_file.app_error", + "translation": "फ़ाइल को निकालने में असमर्थ।" + }, + { + "id": "api.file.remove_directory.app_error", + "translation": "निर्देशिका को निकालने में असमर्थ।" + }, + { + "id": "api.file.read_file.app_error", + "translation": "फ़ाइल को पढ़ने में असमर्थ।" + }, + { + "id": "api.file.move_file.app_error", + "translation": "फ़ाइल ले जाने में असमर्थ।" + }, + { + "id": "api.file.list_directory.app_error", + "translation": "निर्देशिका सूचीबद्ध करने में असमर्थ।" + }, + { + "id": "api.file.file_size.app_error", + "translation": "फ़ाइल का आकार प्राप्त करने में असमर्थ।" + }, + { + "id": "api.file.file_reader.app_error", + "translation": "फ़ाइल रीडर प्राप्त करने में असमर्थ।" + }, + { + "id": "api.file.file_mod_time.app_error", + "translation": "फ़ाइल के लिए अंतिम संशोधन समय प्राप्त करने में असमर्थ।" + }, + { + "id": "api.file.file_exists.app_error", + "translation": "फ़ाइल मौजूद है या नहीं यह जाँचने में असमर्थ।" + }, + { + "id": "api.file.append_file.app_error", + "translation": "फ़ाइल में डेटा जोड़ने में असमर्थ।" + }, + { + "id": "api.export.export_not_found.app_error", + "translation": "निर्यात फ़ाइल ढूँढने में असमर्थ।" + }, + { + "id": "api.custom_status.recent_custom_statuses.delete.app_error", + "translation": "हाल की स्थिति हटाने में विफल। कृपया पहले स्थिति जोड़ने का प्रयास करें या विवरण के लिए अपने सिस्टम व्यवस्थापक से संपर्क करें।" + }, + { + "id": "api.custom_status.disabled", + "translation": "कस्टम स्थिति सुविधा अक्षम कर दी गई है। विवरण के लिए कृपया अपने सिस्टम व्यवस्थापक से संपर्क करें।" + }, + { + "id": "api.config.update_config.not_allowed_security.app_error", + "translation": "सुरक्षा कारणों से {{.Name}} को बदलने की अनुमति नहीं है।" + }, + { + "id": "api.command_custom_status.success", + "translation": "आपकी स्थिति “{{.EmojiName}} {{.StatusMessage}}” पर सेट है। आप चैनल साइडबार हेडर में स्थिति पॉपओवर से अपनी स्थिति बदल सकते हैं।" + }, + { + "id": "api.command_custom_status.name", + "translation": "स्थिति" + }, + { + "id": "api.command_custom_status.hint", + "translation": "[:emoji_name:] [status_message] या साफ़ करें" + }, + { + "id": "api.command_custom_status.desc", + "translation": "अपनी स्थिति सेट या साफ़ करें" + }, + { + "id": "api.command_custom_status.clear.success", + "translation": "आपकी स्थिति साफ़ कर दी गई थी।" + }, + { + "id": "api.command_custom_status.clear.app_error", + "translation": "स्थिति साफ़ करने में त्रुटि।" + }, + { + "id": "Playbooks", + "translation": "प्लेबुक" + }, + { + "id": "plugin_api.send_mail.missing_to", + "translation": "के लिये पता लापता।" + }, + { + "id": "model.guest.is_valid.emails.app_error", + "translation": "अमान्य ईमेल।" + }, + { + "id": "model.guest.is_valid.channels.app_error", + "translation": "अमान्य चैनल।" + }, + { + "id": "app.prepackged-plugin.invalid_version.app_error", + "translation": "पहले से पैक किए गए प्लगइन संस्करण को पार्स नहीं किया जा सका।" + }, + { + "id": "app.member_count", + "translation": "सदस्य संख्या पुनर्प्राप्त करने में त्रुटि" + }, + { + "id": "app.job.get_all_jobs_by_type_and_status.app_error", + "translation": "प्रकार और स्थिति के अनुसार सभी कार्य प्राप्त करने में असमर्थ।" + }, + { + "id": "app.group.crud_permission", + "translation": "उस स्रोत प्रकार के लिए कार्रवाई करने में असमर्थ।" + }, + { + "id": "app.custom_group.unique_name", + "translation": "समूह का नाम अद्वितीय नहीं है" + }, + { + "id": "api.templates.server_inactivity_title", + "translation": "इन शानदार सुविधाओं के साथ बढ़ी हुई उत्पादकता को अनलॉक करें" + }, + { + "id": "api.templates.server_inactivity_subtitle", + "translation": "हे {{.Name}}, हमने देखा है कि आपका मैटरमॉस्ट सर्वर थोड़ी धूल जमा कर रहा है। कुछ विशेषताओं पर एक नज़र डालें जो आपकी टीम के कार्यभार को कम करने में सहायता कर सकती हैं।" + }, + { + "id": "api.templates.server_inactivity_subject", + "translation": "हे! अपनी टीम की उत्पादकता बढ़ाने के लिए मैटरमोस्ट खोलें!" + }, + { + "id": "api.templates.server_inactivity_info_bullet2", + "translation": "का उपयोग करते हुए कार्यों का प्रबंधन करें " + }, + { + "id": "api.templates.server_inactivity_info_bullet1", + "translation": "वर्कफ़्लो प्रबंधन के साथ " + }, + { + "id": "api.templates.server_inactivity_info_bullet", + "translation": "निर्दिष्ट करने के लिए अतिथि पहुँच " + }, + { + "id": "api.templates.server_inactivity_info", + "translation": "आइए और इसे देखिए!" + }, + { + "id": "api.templates.server_inactivity_button", + "translation": "ओपन मैटरमोस्ट" + }, + { + "id": "api.team.invite_members.unable_to_send_email_with_defaults.app_error", + "translation": "SMTP सिस्टम कंसोल में कॉन्फ़िगर नहीं किया गया है" + }, + { + "id": "api.team.invite_members.unable_to_send_email.app_error", + "translation": "ईमेल भेजते समय त्रुटि" + }, + { + "id": "api.server.warn_metric.number_of_teams_5.notification_body", + "translation": "आपके मैटरमॉस्ट सिस्टम में अब कई टीमें हैं। कई टीमों के पास समन्वय और सहयोग करने का अपना पसंदीदा तरीका होता है, जिसमें चैनल कैसे बनाए जाते हैं, नए साथियों को कौन आमंत्रित कर सकता है, और एकीकरण कैसे प्रबंधित किया जाता है। टीम ओवरराइड योजनाएं आपको प्रत्येक टीम के भीतर उपयोगकर्ता अनुमतियों को उनकी विशिष्ट आवश्यकताओं को पूरा करने के लिए अनुकूलित करने की अनुमति देती हैं।\n\n[उन्नत अनुमतियों का उपयोग करने के बारे में अधिक जानें](https://www.mattermost.com/docs-advanced-permissions-team-override/?utm_medium=product&utm_source=mattermost-advisor-bot&utm_content=advanced-permissions-team-override)।\n\nहमसे संपर्क करें क्लिक करके, आप मैटरमोस्ट, इंक. के साथ अपनी जानकारी साझा करेंगे। [अधिक जानें](https://mattermost.com/pl/default-admin-advisory)" + }, + { + "id": "api.server.warn_metric.number_of_active_users_500.notification_body", + "translation": "मैटरमोस्ट दृढ़ता से अनुशंसा करता है कि 500 से अधिक उपयोगकर्ताओं की तैनाती उपयोगकर्ता प्रबंधन, सर्वर क्लस्टरिंग और प्रदर्शन निगरानी जैसी सुविधाओं का लाभ उठाएं। अधिक जानने के लिए हमसे संपर्क करें और हमें बताएं कि हम कैसे मदद कर सकते हैं।\n\nहमसे संपर्क करें क्लिक करके, आप मैटरमोस्ट, इंक. के साथ अपनी जानकारी साझा करेंगे। [अधिक जानें](https://mattermost.com/pl/default-admin-advisory)" + }, + { + "id": "api.license_error", + "translation": "एपीआई समापन बिंदु के लिए लाइसेंस की आवश्यकता होती है" + }, + { + "id": "api.license.client.old_format.app_error", + "translation": "क्लाइंट लाइसेंस के लिए नया प्रारूप अभी समर्थित नहीं है। कृपया क्वेरी स्ट्रिंग में format=old निर्दिष्ट करें।" + }, + { + "id": "api.custom_groups.no_remote_id", + "translation": "remote_id कस्टम समूह के लिए खाली होना चाहिए" + }, + { + "id": "api.custom_groups.must_be_referenceable", + "translation": "कस्टम समूहों के लिए allow_reference 'सत्य' होना चाहिए" + }, + { + "id": "api.custom_groups.license_error", + "translation": "कस्टम समूहों के लिए लाइसेंस प्राप्त नहीं है" + }, + { + "id": "api.custom_groups.feature_disabled", + "translation": "कस्टम समूह सुविधा अक्षम है" + }, + { + "id": "api.custom_groups.count_err", + "translation": "त्रुटि गिनती समूह" + }, + { + "id": "api.unable_to_read_file_from_backend", + "translation": "बैकएंड से फ़ाइल पढ़ने में त्रुटि" + }, + { + "id": "api.unable_to_create_zip_file", + "translation": "ज़िप फ़ाइल बनाने में त्रुटि।" + }, + { + "id": "api.templates.welcome_body.subTitle2", + "translation": "अपना ईमेल पता सत्यापित करने के लिए नीचे क्लिक करें।" + }, + { + "id": "api.templates.welcome_body.subTitle1", + "translation": "जुड़ने के लिए धन्यवाद " + }, + { + "id": "api.templates.welcome_body.info1", + "translation": "यदि यह आप नहीं थे, तो आप सुरक्षित रूप से इस ईमेल को अनदेखा कर सकते हैं।" + }, + { + "id": "api.templates.welcome_body.app_download_title", + "translation": "डेस्कटॉप और मोबाइल ऐप डाउनलोड करें" + }, + { + "id": "api.templates.welcome_body.app_download_button", + "translation": "डाउनलोड" + }, + { + "id": "api.templates.verify_body.subTitle2", + "translation": "अपना ईमेल पता सत्यापित करने के लिए नीचे क्लिक करें।" + }, + { + "id": "api.templates.verify_body.subTitle1", + "translation": "जुड़ने के लिए धन्यवाद " + }, + { + "id": "api.templates.verify_body.serverURL", + "translation": "{{ .ServerURL}}।" + }, + { + "id": "api.templates.verify_body.info1", + "translation": "यदि यह आप नहीं थे, तो आप सुरक्षित रूप से इस ईमेल को अनदेखा कर सकते हैं।" + }, + { + "id": "api.templates.questions_footer.title", + "translation": "प्रशन?" + }, + { + "id": "api.templates.questions_footer.info", + "translation": "हमें किसी भी समय ईमेल करें " + }, + { + "id": "api.templates.payment_failed_no_card.title", + "translation": "आपका सबसे महत्वपूर्ण क्लाउड चालान देय है" + }, + { + "id": "api.templates.payment_failed_no_card.subject", + "translation": "आपकी मैटरमोस्ट क्लाउड सदस्यता के लिए भुगतान देय है" + }, + { + "id": "api.templates.payment_failed_no_card.info3", + "translation": "पोस्ट करते समय इस एकीकरण के प्रोफ़ाइल चित्र के लिए .png या .jpg फ़ाइल का URL दर्ज करें। फ़ाइल कम से कम 128 पिक्सेल गुणा 128 पिक्सेल की होनी चाहिए. यदि खाली छोड़ दिया जाता है, तो वेबहुक निर्माता द्वारा निर्दिष्ट प्रोफ़ाइल चित्र का उपयोग किया जाता है।" + }, + { + "id": "api.oauth.get_access_token.redirect_uri.app_error", + "translation": "अमान्य_अनुरोध: आपूर्ति की गई रीडायरेक्ट_यूरी प्राधिकरण कोड रीडायरेक्ट_यूरी से मेल नहीं खाती।" + }, + { + "id": "Channels", + "translation": "चैनल" + }, + { + "id": "api.server.warn_metric.number_of_active_users_100.start_trial.notification_body", + "translation": "आपके मैटरमॉस्ट सिस्टम में 100 से अधिक उपयोगकर्ता हैं। जैसे-जैसे आपका उपयोगकर्ता आधार बढ़ता है, नए खातों का प्रावधान करना समय लेने वाला हो सकता है। हम अनुशंसा करते हैं कि आप अपने संगठन की सक्रिय निर्देशिका/एलडीएपी को एकीकृत करें, जो किसी भी व्यक्ति को खाते के साथ मैटरमोस्ट तक पहुंचने की अनुमति देगा।\n\n[AD/LDAP के साथ एकीकरण के बारे में अधिक जानें](https://www.mattermost.com/docs-adldap/?utm_medium=product&utm_source=mattermost-advisor-bot&utm_content=adldap)\n\nपरीक्षण प्रारंभ करें क्लिक करके, मैं [Mattermost सॉफ़्टवेयर मूल्यांकन अनुबंध](https://mattermost.com/software-evaluation-agreement/), [गोपनीयता नीति](https://mattermost.com/privacy-policy/) से सहमत हूं , और उत्पाद ईमेल प्राप्त करना।" + }, + { + "id": "api.server.warn_metric.mfa.start_trial.notification_body", + "translation": "आपके मैटरमॉस्ट सिस्टम में बहु-कारक प्रमाणीकरण सक्षम है, जो उपयोगकर्ताओं को पासवर्ड से परे प्रमाणीकरण के अतिरिक्त साधनों के साथ अपने खातों को सुरक्षित करने का विकल्प देता है। पूरे सिस्टम में सुरक्षा में सुधार के लिए आपको सभी मैटरमोस्ट खातों को बहु-कारक प्रमाणीकरण का उपयोग करने की आवश्यकता हो सकती है।\n\n[मल्टी-फैक्टर ऑथेंटिकेशन लागू करने के बारे में ज़्यादा जानें](https://www.mattermost.com/docs-multi-factor-authentication/?utm_medium=product&utm_source=mattermost-advisor-bot&utm_content=multi-factor-authentication)\n\nपरीक्षण प्रारंभ करें क्लिक करके, मैं [Mattermost सॉफ़्टवेयर मूल्यांकन अनुबंध](https://mattermost.com/software-evaluation-agreement/), [गोपनीयता नीति](https://mattermost.com/privacy-policy/) से सहमत हूं , और उत्पाद ईमेल प्राप्त करना।" + }, + { + "id": "api.server.warn_metric.number_of_active_users_100.notification_body", + "translation": "आपके मैटरमॉस्ट सिस्टम में 100 से अधिक उपयोगकर्ता हैं। जैसे-जैसे आपका उपयोगकर्ता आधार बढ़ता है, नए खातों का प्रावधान करना समय लेने वाला हो सकता है। हम अनुशंसा करते हैं कि आप अपने संगठन की सक्रिय निर्देशिका/एलडीएपी को एकीकृत करें, जो किसी भी व्यक्ति को खाते के साथ मैटरमोस्ट तक पहुंचने की अनुमति देगा।\n\n[AD/LDAP के साथ एकीकरण के बारे में अधिक जानें](https://www.mattermost.com/docs-adldap/?utm_medium=product&utm_source=mattermost-advisor-bot&utm_content=adldap)\n\nहमसे संपर्क करें क्लिक करके, आप मैटरमोस्ट, इंक. के साथ अपनी जानकारी साझा करेंगे। [अधिक जानें](https://mattermost.com/pl/default-admin-advisory)" + }, + { + "id": "api.server.warn_metric.number_of_active_users_200.notification_body", + "translation": "आपके मैटरमॉस्ट सिस्टम में अब 200 उपयोगकर्ता हैं। जब आप मैटरमोस्ट को अपने संगठन के एकल साइन-ऑन प्रदाता से कनेक्ट करते हैं, तो उपयोगकर्ता अपने क्रेडेंशियल्स को फिर से दर्ज किए बिना मैटरमॉस्ट तक पहुंच सकते हैं। हम अनुशंसा करते हैं कि आप अपने एसएएमएल 2.0 प्रदाता को अपने मैटरमोस्ट सर्वर के साथ एकीकृत करें। .\n\nहमसे संपर्क करें क्लिक करके, आप मैटरमोस्ट, इंक. के साथ अपनी जानकारी साझा करेंगे। [अधिक जानें](https://mattermost.com/pl/default-admin-advisory)" + }, + { + "id": "api.server.warn_metric.number_of_active_users_200.start_trial.notification_body", + "translation": "आपके मैटरमॉस्ट सिस्टम में अब 200 उपयोगकर्ता हैं। जब आप मैटरमॉस्ट को अपने संगठन के एकल साइन-ऑन प्रदाता से कनेक्ट करते हैं, तो उपयोगकर्ता अपने क्रेडेंशियल्स को फिर से दर्ज किए बिना मैटरमॉस्ट तक पहुंच सकते हैं। हम अनुशंसा करते हैं कि आप अपने एसएएमएल 2.0 प्रदाता को अपने मैटरमोस्ट सर्वर के साथ एकीकृत करें।\n\nपरीक्षण प्रारंभ करें क्लिक करके, मैं [Mattermost सॉफ़्टवेयर मूल्यांकन अनुबंध](https://mattermost.com/software-evaluation-agreement/), [गोपनीयता नीति](https://mattermost.com/privacy-policy/) से सहमत हूं , और उत्पाद ईमेल प्राप्त करना।" + }, + { + "id": "api.server.warn_metric.number_of_teams_5.start_trial.notification_body", + "translation": "आपके मैटरमॉस्ट सिस्टम में अब कई टीमें हैं। कई टीमों के पास समन्वय और सहयोग करने का अपना पसंदीदा तरीका होता है, जिसमें चैनल कैसे बनाए जाते हैं, नए साथियों को कौन आमंत्रित कर सकता है, और एकीकरण कैसे प्रबंधित किया जाता है। टीम ओवरराइड योजनाएं आपको प्रत्येक टीम के भीतर उपयोगकर्ता अनुमतियों को उनकी विशिष्ट आवश्यकताओं को पूरा करने के लिए अनुकूलित करने की अनुमति देती हैं।\n\n[उन्नत अनुमतियों का उपयोग करने के बारे में अधिक जानें](https://www.mattermost.com/docs-advanced-permissions-team-override/?utm_medium=product&utm_source=mattermost-advisor-bot&utm_content=advanced-permissions-team-override)\n\nपरीक्षण प्रारंभ करें क्लिक करके, मैं [Mattermost सॉफ़्टवेयर मूल्यांकन अनुबंध](https://mattermost.com/software-evaluation-agreement/), [गोपनीयता नीति](https://mattermost.com/privacy-policy/) से सहमत हूं , और उत्पाद ईमेल प्राप्त करना।" + }, + { + "id": "api.server.warn_metric.number_of_posts_2M.start_trial.notification_body", + "translation": "आपके मैटरमॉस्ट सिस्टम में बड़ी संख्या में संदेश हैं। डिफ़ॉल्ट मैटरमॉस्ट डेटाबेस खोज लगभग 2.5 मिलियन पोस्ट पर प्रदर्शन में गिरावट दिखाना शुरू कर देती है। 5 मिलियन से अधिक पोस्ट के साथ, Elasticsearch महत्वपूर्ण प्रदर्शन समस्याओं से बचने में मदद कर सकता है, जैसे कि टाइमआउट, खोज और उल्लेख के साथ। अधिक जानने के लिए हमसे संपर्क करें और हमें बताएं कि हम कैसे मदद कर सकते हैं।\n\n[प्रदर्शन सुधारने के बारे में और जानें](https://www.mattermost.com/docs-elasticsearch/?utm_medium=product&utm_source=mattermost-advisor-bot&utm_content=elasticsearch)\n\nपरीक्षण प्रारंभ करें क्लिक करके, मैं [Mattermost सॉफ़्टवेयर मूल्यांकन अनुबंध](https://mattermost.com/software-evaluation-agreement/), [गोपनीयता नीति](https://mattermost.com/privacy-policy/) से सहमत हूं , और उत्पाद ईमेल प्राप्त करना।" + }, + { + "id": "api.server.warn_metric.number_of_posts_2M.notification_body", + "translation": "आपके मैटरमॉस्ट सिस्टम में बड़ी संख्या में संदेश हैं। डिफ़ॉल्ट मैटरमॉस्ट डेटाबेस खोज लगभग 2.5 मिलियन पोस्ट पर प्रदर्शन में गिरावट दिखाना शुरू कर देती है। 5 मिलियन से अधिक पोस्ट के साथ, Elasticsearch महत्वपूर्ण प्रदर्शन समस्याओं से बचने में मदद कर सकता है, जैसे कि टाइमआउट, खोज और उल्लेख के साथ। अधिक जानने के लिए हमसे संपर्क करें और हमें बताएं कि हम कैसे मदद कर सकते हैं।\n\n[प्रदर्शन सुधारने के बारे में और जानें](https://www.mattermost.com/docs-elasticsearch/?utm_medium=product&utm_source=mattermost-advisor-bot&utm_content=elasticsearch)\n\nहमसे संपर्क करें क्लिक करके, आप मैटरमोस्ट, इंक. के साथ अपनी जानकारी साझा करेंगे। [अधिक जानें](https://mattermost.com/pl/default-admin-advisory)" + }, + { + "id": "api.server.warn_metric.number_of_channels_50.start_trial.notification_body", + "translation": "चैनल संचार को बेहतर बनाने में मदद करते हैं, लेकिन मैटरमोस्ट के उपयोगकर्ताओं के साथ जुड़ने और चैनल बनाने के साथ, सिस्टम को व्यवस्थित रखने की चुनौती बढ़ जाती है। उन्नत अनुमतियां आपको यह सेट करने में सक्षम बनाती हैं कि कौन से उपयोगकर्ता या भूमिकाएं कुछ क्रियाएं कर सकती हैं, जिसमें चैनल सेटिंग और सदस्यों को प्रबंधित करना, उपयोगकर्ताओं के व्यापक समूहों को टैग करने के लिए @चैनल या @here का उपयोग करना और नए वेबहुक बनाना शामिल है।\n\n[उन्नत अनुमतियों का उपयोग करने के बारे में और जानें](https://www.mattermost.com/docs-advanced-permissions/?utm_medium=product&utm_source=mattermost-advisor-bot&utm_content=advanced-permissions)\n\nपरीक्षण प्रारंभ करें क्लिक करके, मैं [Mattermost सॉफ़्टवेयर मूल्यांकन अनुबंध](https://mattermost.com/software-evaluation-agreement/), [गोपनीयता नीति](https://mattermost.com/privacy-policy/) से सहमत हूं , और उत्पाद ईमेल प्राप्त करना।" + }, + { + "id": "api.server.warn_metric.number_of_channels_50.notification_body", + "translation": "चैनल संचार को बेहतर बनाने में मदद करते हैं, लेकिन मैटरमोस्ट के उपयोगकर्ताओं के साथ जुड़ने और चैनल बनाने के साथ, सिस्टम को व्यवस्थित रखने की चुनौती बढ़ जाती है। उन्नत अनुमतियां आपको यह सेट करने में सक्षम बनाती हैं कि कौन से उपयोगकर्ता या भूमिकाएं कुछ क्रियाएं कर सकती हैं, जिसमें चैनल सेटिंग और सदस्यों को प्रबंधित करना, उपयोगकर्ताओं के व्यापक समूहों को टैग करने के लिए @चैनल या @here का उपयोग करना और नए वेबहुक बनाना शामिल है।\n\n[उन्नत अनुमतियों का उपयोग करने के बारे में और जानें](https://www.mattermost.com/docs-advanced-permissions/?utm_medium=product&utm_source=mattermost-advisor-bot&utm_content=advanced-permissions)\n\nहमसे संपर्क करें क्लिक करके, आप मैटरमोस्ट, इंक. के साथ अपनी जानकारी साझा करेंगे। [अधिक जानें](https://mattermost.com/pl/default-admin-advisory)" + }, + { + "id": "api.server.warn_metric.number_of_active_users_500.start_trial.notification_body", + "translation": "मैटरमोस्ट दृढ़ता से अनुशंसा करता है कि 500 से अधिक उपयोगकर्ताओं की तैनाती उपयोगकर्ता प्रबंधन, सर्वर क्लस्टरिंग और प्रदर्शन निगरानी जैसी सुविधाओं का लाभ उठाएं। अधिक जानने के लिए हमसे संपर्क करें और हमें बताएं कि हम कैसे मदद कर सकते हैं।\n\nपरीक्षण प्रारंभ करें क्लिक करके, मैं [Mattermost सॉफ़्टवेयर मूल्यांकन अनुबंध](https://mattermost.com/software-evaluation-agreement/), [गोपनीयता नीति](https://mattermost.com/privacy-policy/) से सहमत हूं , और उत्पाद ईमेल प्राप्त करना।" + }, + { + "id": "api.server.warn_metric.number_of_active_users_300.start_trial.notification_body", + "translation": "मैटरमॉस्ट में इतनी अधिक बातचीत होने के कारण, यह जानना चुनौतीपूर्ण हो सकता है कि महत्वपूर्ण जानकारी को कहाँ खोजा जाए। यदि आप एक संदेश को बड़े दर्शकों तक प्रसारित करना चाहते हैं, तो आप केवल-पढ़ने के लिए घोषणा चैनल सेट कर सकते हैं, जहां कोई भी शामिल हो सकता है, लेकिन केवल चैनल व्यवस्थापक ही संदेश पोस्ट कर सकते हैं।\n\n[केवल पढ़ने के लिए घोषणा चैनल बनाने के बारे में अधिक जानें](https://www.mattermost.com/docs-channel-moderation/?utm_medium=product&utm_source=mattermost-advisor-bot&utm_content=channel-moderation)\n\nपरीक्षण प्रारंभ करें क्लिक करके, मैं [Mattermost सॉफ़्टवेयर मूल्यांकन अनुबंध](https://mattermost.com/software-evaluation-agreement/), [गोपनीयता नीति](https://mattermost.com/privacy-policy/) से सहमत हूं , और उत्पाद ईमेल प्राप्त करना।" + }, + { + "id": "api.server.warn_metric.number_of_active_users_300.notification_body", + "translation": "मैटरमॉस्ट में इतनी अधिक बातचीत होने के कारण, यह जानना चुनौतीपूर्ण हो सकता है कि महत्वपूर्ण जानकारी को कहाँ खोजा जाए। यदि आप एक संदेश को बड़े दर्शकों तक प्रसारित करना चाहते हैं, तो आप केवल-पढ़ने के लिए घोषणा चैनल सेट कर सकते हैं, जहां कोई भी शामिल हो सकता है, लेकिन केवल चैनल व्यवस्थापक ही संदेश पोस्ट कर सकते हैं।\n\n[केवल पढ़ने के लिए घोषणा चैनल बनाने के बारे में अधिक जानें](https://www.mattermost.com/docs-channel-moderation/?utm_medium=product&utm_source=mattermost-advisor-bot&utm_content=channel-moderation)\n\nहमसे संपर्क करें क्लिक करके, आप मैटरमोस्ट, इंक. के साथ अपनी जानकारी साझा करेंगे। [अधिक जानें](https://mattermost.com/pl/default-admin-advisory)" + }, + { + "id": "api.server.warn_metric.email_domain.start_trial.notification_body", + "translation": "परियोजनाओं में अक्सर संगठन के अंदर और बाहर दोनों जगह के लोग शामिल होते हैं। अतिथि खातों के साथ, आप बाहरी भागीदारों को अपने मैटरमोस्ट सिस्टम में ला सकते हैं और निर्दिष्ट कर सकते हैं कि वे किसके साथ काम कर सकते हैं और क्या देख सकते हैं।\n\n[अतिथि खातों को सक्षम करने के बारे में और जानें](https://www.mattermost.com/docs-guest-accounts/?utm_medium=product&utm_source=mattermost-advisor-bot&utm_content=guest-accounts)\n\nपरीक्षण प्रारंभ करें क्लिक करके, मैं [Mattermost सॉफ़्टवेयर मूल्यांकन अनुबंध](https://mattermost.com/software-evaluation-agreement/), [गोपनीयता नीति](https://mattermost.com/privacy-policy/) से सहमत हूं , और उत्पाद ईमेल प्राप्त करना।" + }, + { + "id": "api.server.warn_metric.mfa.notification_body", + "translation": "आपके मैटरमॉस्ट सिस्टम में बहु-कारक प्रमाणीकरण सक्षम है, जो उपयोगकर्ताओं को पासवर्ड से परे प्रमाणीकरण के अतिरिक्त साधनों के साथ अपने खातों को सुरक्षित करने का विकल्प देता है। पूरे सिस्टम में सुरक्षा को बेहतर बनाने के लिए आपको सभी मैटरमोस्ट खातों को बहु-कारक प्रमाणीकरण का उपयोग करने की आवश्यकता हो सकती है।\n\n[मल्टी-फैक्टर ऑथेंटिकेशन लागू करने के बारे में और जानें](https://www.mattermost.com/docs-multi-factor-authentication/?utm_medium=product&utm_source=mattermost-advisor-bot&utm_content=multi-factor-authentication)।\n\nहमसे संपर्क करें क्लिक करके, आप मैटरमोस्ट, इंक. के साथ अपनी जानकारी साझा करेंगे। [अधिक जानें](https://mattermost.com/pl/default-admin-advisory)" + }, + { + "id": "api.command_marketplace.unsupported.app_error", + "translation": "मार्केटप्लेस कमांड आपके डिवाइस पर समर्थित नहीं है।" + }, + { + "id": "api.command_marketplace.name", + "translation": "बाजार" + }, + { + "id": "api.command_marketplace.desc", + "translation": "बाज़ार खोलें" + }, + { + "id": "api.command_help.success", + "translation": "मैटरमोस्ट सुरक्षित संचार, सहयोग और उपकरणों और टीमों के काम के ऑर्केस्ट्रेशन के लिए एक खुला स्रोत मंच है।\nमैटरमोस्ट में तीन प्रमुख उपकरण हैं:\n\n**चैनल** - 1:1 और समूह संदेश के माध्यम से अपनी टीम से जुड़े रहें।\n**[प्लेबुक](/प्लेबुक)** - विशिष्ट और अनुमानित परिणाम प्राप्त करने के लिए दोहराने योग्य प्रक्रियाओं का निर्माण और कॉन्फ़िगर करें।\n**[Boards](/boards)** - अपनी टीम को प्रमुख मील के पत्थर हासिल करने में मदद करने के लिए कानबन बोर्ड संरचना में परियोजनाओं और कार्यों को प्रबंधित करें।\n\n[दस्तावेज़ीकरण और मार्गदर्शिकाएँ देखें]({{.HelpLink}})" + }, + { + "id": "api.cloud.teams_limit_reached.restore", + "translation": "टीम को बहाल करने में असमर्थ क्योंकि टीमों की सीमा पूरी हो चुकी है" + }, + { + "id": "api.cloud.teams_limit_reached.create", + "translation": "टीम बनाने में असमर्थ क्योंकि टीमों की सीमा पूरी हो चुकी है" + }, + { + "id": "api.cloud.notify_admin_to_upgrade_error.already_notified", + "translation": "व्यवस्थापक को पहले ही सूचित कर दिया गया है" + }, + { + "id": "api.cloud.delinquency_email.missing_email_to_trigger", + "translation": "चूक ईमेल भेजने के लिए आवश्यक फ़ील्ड गुम हैं।" + }, + { + "id": "api.admin.syncables_error", + "translation": "उपयोगकर्ता को समूह-टीमों और समूह-चैनलों में जोड़ने में विफल रहा" + }, + { + "id": "api.acknowledgement.save.archived_channel.app_error", + "translation": "आप संग्रहीत चैनल में पावती नहीं दे सकते।" + }, + { + "id": "api.acknowledgement.delete.deadline.app_error", + "translation": "5 मिनट बीत जाने के बाद आप पावती नहीं हटा सकते।" + }, + { + "id": "api.acknowledgement.delete.archived_channel.app_error", + "translation": "आप संग्रहीत चैनल में पावती नहीं निकाल सकते।" + } +] diff --git a/server/i18n/hr.json b/server/i18n/hr.json new file mode 100644 index 00000000000..660fa2fb63b --- /dev/null +++ b/server/i18n/hr.json @@ -0,0 +1,662 @@ +[ + { + "id": "ent.saml.service_disable.app_error", + "translation": "SAML 2.0 nije konfiguriran ili podržan na ovom poslužitelju." + }, + { + "id": "api.user.upload_profile_user.too_large.app_error", + "translation": "Nije moguće postaviti datoteku. Datoteka je prevelika." + }, + { + "id": "api.user.upload_profile_user.storage.app_error", + "translation": "Nije moguće postaviti sliku. Potrebno je urediti postavke za pohranu." + }, + { + "id": "api.user.upload_profile_user.parse.app_error", + "translation": "Neuspjela obrada višedjelnog obrasca." + }, + { + "id": "api.user.upload_profile_user.open.app_error", + "translation": "Neuspjelo otvaranje slikovne datoteke." + }, + { + "id": "api.user.upload_profile_user.no_file.app_error", + "translation": "Nedostaje datoteka pod 'image' u zahtjevu." + }, + { + "id": "api.user.upload_profile_user.array.app_error", + "translation": "Prazan niz pod 'image' u zahtjevu." + }, + { + "id": "api.user.saml.not_available.app_error", + "translation": "SAML 2.0 nije konfiguriran ili podržan na ovom poslužitelju." + }, + { + "id": "api.team.set_team_icon.too_large.app_error", + "translation": "Nije moguće postaviti datoteku. Datoteka je prevelika." + }, + { + "id": "api.team.set_team_icon.storage.app_error", + "translation": "Nije moguće postaviti sliku. Potrebno je urediti postavke za pohranu." + }, + { + "id": "api.team.set_team_icon.parse.app_error", + "translation": "Neuspjela obrada višedjelnog obrasca." + }, + { + "id": "api.team.set_team_icon.open.app_error", + "translation": "Neuspjelo otvaranje slikovne datoteke." + }, + { + "id": "api.team.set_team_icon.no_file.app_error", + "translation": "Nedostaje datoteka pod 'image' u zahtjevu." + }, + { + "id": "api.team.set_team_icon.array.app_error", + "translation": "Prazan niz pod 'image' u zahtjevu." + }, + { + "id": "api.team.import_team.parse.app_error", + "translation": "Neuspjela obrada višedjelnog obrasca." + }, + { + "id": "api.team.import_team.open.app_error", + "translation": "Neuspjelo otvaranje datoteke." + }, + { + "id": "api.team.import_team.no_file.app_error", + "translation": "Nedostaje datoteka pod 'file' u zahtjevu." + }, + { + "id": "api.team.import_team.array.app_error", + "translation": "Prazan niz pod 'file' u zahtjevu." + }, + { + "id": "api.license.add_license.open.app_error", + "translation": "Neuspjelo otvaranje datoteke licence." + }, + { + "id": "api.license.add_license.no_file.app_error", + "translation": "Nedostaje datoteka pod 'license' u zahtjevu." + }, + { + "id": "api.license.add_license.array.app_error", + "translation": "Prazan niz pod 'license' u zahtjevu." + }, + { + "id": "api.file.upload_file.storage.app_error", + "translation": "Nije moguće postaviti sliku. Potrebno je urediti postavke za pohranu." + }, + { + "id": "api.admin.upload_brand_image.too_large.app_error", + "translation": "Nije moguće postaviti datoteku. Datoteka je prevelika." + }, + { + "id": "api.admin.upload_brand_image.storage.app_error", + "translation": "Nije moguće postaviti sliku. Potrebno je urediti postavke za pohranu." + }, + { + "id": "api.admin.upload_brand_image.parse.app_error", + "translation": "Neuspjela obrada višedjelnog obrasca." + }, + { + "id": "api.admin.upload_brand_image.no_file.app_error", + "translation": "Nedostaje datoteka pod 'image' i zahtjevu." + }, + { + "id": "api.admin.upload_brand_image.array.app_error", + "translation": "Prazan niz pod 'image' u zahtjevu." + }, + { + "id": "api.admin.test_email.subject", + "translation": "Mattermost – Testiranje postavki e-pošte" + }, + { + "id": "api.admin.test_email.reenter_password", + "translation": "SMTP server, port, ili korisničko ime su promijenjeni. Molimo ponovno unesite SMTP lozinku za testiranje veze." + }, + { + "id": "api.admin.test_email.missing_server", + "translation": "Potreban je SMTP server" + }, + { + "id": "api.admin.test_email.body", + "translation": "Čini se da je tvoja Mattermost e-pošta pravilno postavljena!" + }, + { + "id": "api.admin.saml.not_available.app_error", + "translation": "SAML 2.0 nije postavljen ili podržan na ovom poslužitelju." + }, + { + "id": "api.admin.saml.failure_save_idp_certificate_file.app_error", + "translation": "Neuspjelo spremanje datoteke certifikata." + }, + { + "id": "api.admin.get_brand_image.storage.app_error", + "translation": "Spremište slika nije konfigurirano." + }, + { + "id": "api.admin.file_read_error", + "translation": "Greška u čitanju datoteke dnevnika." + }, + { + "id": "api.admin.add_certificate.saving.app_error", + "translation": "Neuspjelo spremanje datoteke certifikata." + }, + { + "id": "api.admin.add_certificate.open.app_error", + "translation": "Neuspjelo otvaranje datoteke certifikata." + }, + { + "id": "api.admin.add_certificate.no_file.app_error", + "translation": "U zahtjevu nema datoteke u polju 'certifikat'." + }, + { + "id": "api.admin.add_certificate.array.app_error", + "translation": "U zahtjevu nema datoteke u polju 'certifikat'." + }, + { + "id": "September", + "translation": "rujan" + }, + { + "id": "October", + "translation": "listopad" + }, + { + "id": "November", + "translation": "studeni" + }, + { + "id": "May", + "translation": "svibanj" + }, + { + "id": "March", + "translation": "ožujak" + }, + { + "id": "June", + "translation": "lipanj" + }, + { + "id": "July", + "translation": "srpanj" + }, + { + "id": "January", + "translation": "siječanj" + }, + { + "id": "February", + "translation": "veljača" + }, + { + "id": "December", + "translation": "prosinac" + }, + { + "id": "August", + "translation": "kolovoz" + }, + { + "id": "April", + "translation": "travanj" + }, + { + "id": "api.templates.cloud_welcome_email.button", + "translation": "Otvori Mattermost" + }, + { + "id": "api.templates.cloud_welcome_email.app_market_place", + "translation": "tržnici programa." + }, + { + "id": "api.templates.cloud_welcome_email.add_apps_sub_info", + "translation": "Optimiraj tok tvog rada pomoću alata kao što su GitHub, Jira i Zoom. Istraži sve naše integracije na" + }, + { + "id": "Playbooks", + "translation": "Priručnici" + }, + { + "id": "Channels", + "translation": "Kanali" + }, + { + "id": "Boards", + "translation": "Ploče" + }, + { + "id": "api.admin.ldap.not_available.app_error", + "translation": "LDAP nije dostupan." + }, + { + "id": "api.admin.saml.failure_get_metadata_from_idp.app_error", + "translation": "Neuspjelo dobivanje metapodataka s URL-a pružatelja usluge identifikacije." + }, + { + "id": "api.admin.remove_certificate.delete.app_error", + "translation": "Dogodila se greška tijekom brisanja certifikata." + }, + { + "id": "api.admin.delete_brand_image.storage.not_found", + "translation": "Nije moguće izbrisati sliku brenda, nije pronađena." + }, + { + "id": "api.admin.add_certificate.parseform.app_error", + "translation": "Greška u obradi zahtjeva s višebrojnim obrascima" + }, + { + "id": "api.back_to_app", + "translation": "Natrag na {{.SiteName}}" + }, + { + "id": "api.admin.test_s3.missing_s3_bucket", + "translation": "Potreban je S3 Bucket" + }, + { + "id": "api.admin.saml.set_certificate_from_metadata.missing_content_type.app_error", + "translation": "Nedostaje vrsta sadržaja." + }, + { + "id": "api.admin.saml.set_certificate_from_metadata.invalid_content_type.app_error", + "translation": "Nevaljana vrsta sadržaja." + }, + { + "id": "api.admin.saml.set_certificate_from_metadata.invalid_body.app_error", + "translation": "Nevaljani tekst certifikata." + }, + { + "id": "api.admin.saml.metadata.app_error", + "translation": "Došlo je do greške prilikom izgradnje metapodataka pružatelja usluga." + }, + { + "id": "api.admin.saml.invalid_xml_missing_ssoservices.app_error", + "translation": "Nedostaje čvor SSO usluga pružatelja identiteta u XML-u." + }, + { + "id": "api.admin.saml.invalid_xml_missing_keydescriptor.app_error", + "translation": "Nedostaje ključ SSO deskriptora pružatelja identiteta u XML-u." + }, + { + "id": "api.admin.saml.invalid_xml_missing_idpssodescriptors.app_error", + "translation": "Nedostaje čvor SSO deskriptora pružatelja identiteta u XML-u." + }, + { + "id": "api.admin.saml.failure_parse_idp_certificate.app_error", + "translation": "Greška prilikom obrade informacija metapodataka primljenih od pružatelja identiteta za certifikat." + }, + { + "id": "api.admin.saml.failure_reset_authdata_to_email.app_error", + "translation": "Neuspjelo resetiranje polja podataka autentifikacije za e-poštu." + }, + { + "id": "app.import.validate_user_import_data.auth_data_and_password.error", + "translation": "Korisnički podaci autentifikacije i lozinka su međusobno isključivi." + }, + { + "id": "app.import.validate_user_import_data.auth_data_and_service_dependency.error", + "translation": "Korisnički podaci autentifikacije i podaci autentifikacije su međusobno uključivi." + }, + { + "id": "app.import.validate_user_import_data.auth_data_length.error", + "translation": "Korisnički podaci autentifikacije su predugački." + }, + { + "id": "api.user.update_user_auth.invalid_request", + "translation": "Zahtjevu nedostaje parametar podatka autentifikacije ili usluge autentifikacije." + }, + { + "id": "api.channel.add_user.to.channel.failed.app_error", + "translation": "Neuspjelo dodavanje korisnika u kanal." + }, + { + "id": "api.channel.add_members.error", + "translation": "Greška u dodavanju članova kanala." + }, + { + "id": "api.channel.add_member.added", + "translation": "%v je dodan/a u kanal od %v." + }, + { + "id": "api.channel.add_guest.added", + "translation": "%v je dodan/a u kanal kao gost od %v." + }, + { + "id": "api.bot.create_disabled", + "translation": "Stvaranje bota je deaktivirano." + }, + { + "id": "api.command_msg.name", + "translation": "poruka" + }, + { + "id": "api.command_leave.name", + "translation": "napusti" + }, + { + "id": "api.command_kick.name", + "translation": "izbaci" + }, + { + "id": "api.command_join.name", + "translation": "pridruži se" + }, + { + "id": "api.command_invite.name", + "translation": "pozovi" + }, + { + "id": "api.command_help.name", + "translation": "pomoć" + }, + { + "id": "api.command_groupmsg.name", + "translation": "poruka" + }, + { + "id": "api.command_expand.name", + "translation": "rasklopi" + }, + { + "id": "api.command_code.name", + "translation": "kȏd" + }, + { + "id": "api.command_dnd.name", + "translation": "ne smetaj" + }, + { + "id": "api.command_dnd.desc", + "translation": "„Ne smetaj” deaktivira automatsko slanje obavijesti na radnu površinu i mobilni uređaj." + }, + { + "id": "api.command_custom_status.name", + "translation": "stanje" + }, + { + "id": "api.command_code.hint", + "translation": "[tekst]" + }, + { + "id": "api.command_custom_status.hint", + "translation": "[:emoji_name:] [status_message] ili izbriši" + }, + { + "id": "api.command_custom_status.desc", + "translation": "Postavi ili izbriši tvoje stanje" + }, + { + "id": "api.command_collapse.success", + "translation": "Poveznice na slike se sada standardno sklapaju" + }, + { + "id": "api.command_collapse.name", + "translation": "sklopi" + }, + { + "id": "api.command_channel_rename.message.app_error", + "translation": "Mora se zadati poruka s naredbom /preimenuj." + }, + { + "id": "api.command_channel_rename.name", + "translation": "preimenuj" + }, + { + "id": "api.command_channel_rename.hint", + "translation": "[tekst]" + }, + { + "id": "api.command_channel_rename.direct_group.app_error", + "translation": "Nije moguće preimenovati kanale izravnih poruka." + }, + { + "id": "api.command_channel_rename.desc", + "translation": "Preimenuj kanal" + }, + { + "id": "api.command_channel_purpose.name", + "translation": "svrha" + }, + { + "id": "api.command_channel_purpose.hint", + "translation": "[tekst]" + }, + { + "id": "api.command_channel_header.name", + "translation": "zaglavlje" + }, + { + "id": "api.command_channel_header.message.app_error", + "translation": "Tekst se mora navesti s naredbom /header." + }, + { + "id": "api.command_channel_header.hint", + "translation": "[tekst]" + }, + { + "id": "api.command_channel_header.desc", + "translation": "Uredi zaglavlje kanala" + }, + { + "id": "api.command_away.success", + "translation": "Sada si odsutan/na" + }, + { + "id": "api.command_away.name", + "translation": "odsutan" + }, + { + "id": "api.command.invite_people.invite_off", + "translation": "Stvaranje korisnika je deaktivirano na ovom poslužitelju, nijedna pozivnica nije poslana" + }, + { + "id": "api.command.invite_people.hint", + "translation": "[ime@domena.hr …]" + }, + { + "id": "api.command.invite_people.fail", + "translation": "Dogodila se greška tijekom slanja e-mail pozivnica" + }, + { + "id": "api.command.invite_people.email_off", + "translation": "E-pošta nije konfigurirna, nijedna pozivnica nije poslana" + }, + { + "id": "api.command.invite_people.sent", + "translation": "E-mail pozivnice su poslane" + }, + { + "id": "api.command.invite_people.email_invitations_off", + "translation": "E-mail pozivnice su deaktivirane, nijedna pozivnica nije poslana" + }, + { + "id": "api.command.invite_people.desc", + "translation": "Pošalji e-mail pozivnicu tvom Mattermost timu" + }, + { + "id": "api.channel.remove_member.removed", + "translation": "%v je uklonjen/a iz kanala." + }, + { + "id": "api.channel.remove_channel_member.type.app_error", + "translation": "Nije moguće ukloniti korisnika iz kanala." + }, + { + "id": "api.channel.remove.default.app_error", + "translation": "Nije moguće ukloniti korisnika iz standardnog kanala {{.Channel}}." + }, + { + "id": "api.channel.post_user_add_remove_message_and_forget.error", + "translation": "Neuspjelo objavljivanje poruke o pridruživanju/napuštanju" + }, + { + "id": "api.channel.post_update_channel_header_message_and_forget.updated_to", + "translation": "%s je aktualizirao/la zaglavlje kanala u: %s" + }, + { + "id": "api.channel.post_update_channel_header_message_and_forget.updated_from", + "translation": "%s je aktualizirao/la zaglavlje kanala iz: %s u: %s" + }, + { + "id": "api.channel.post_update_channel_header_message_and_forget.removed", + "translation": "%s je uklonio/la zaglavlje kanala (bilo je: %s)" + }, + { + "id": "api.channel.post_update_channel_header_message_and_forget.post.error", + "translation": "Neuspjela objava poruke o aktualiziranju zaglavlja kanala" + }, + { + "id": "api.channel.post_update_channel_displayname_message_and_forget.updated_from", + "translation": "%s je aktualizirao/la ime kanala iz: %s u: %s" + }, + { + "id": "api.channel.post_update_channel_displayname_message_and_forget.create_post.error", + "translation": "Neuspjela objava poruke o aktualiziranju prikaznog imena" + }, + { + "id": "api.channel.post_channel_privacy_message.error", + "translation": "Neuspjela objava poruke o aktualiziranju privatnog kanala." + }, + { + "id": "api.channel.patch_update_channel.forbidden.app_error", + "translation": "Neuspjelo aktualiziranje kanala." + }, + { + "id": "api.channel.patch_channel_moderations.license.error", + "translation": "Tvoja licenca ne podržava moderiranje kanala" + }, + { + "id": "api.channel.move_channel.type.invalid", + "translation": "Nije moguće premjestiti izravne ili grupne kanale poruka" + }, + { + "id": "api.channel.leave.left", + "translation": "%v je napustio/la kanal." + }, + { + "id": "api.channel.leave.last_member.app_error", + "translation": "Ti si jedini preostali član. Pokušaj ukloniti privatni kanal umjesto da ga napustiš." + }, + { + "id": "api.channel.leave.direct.app_error", + "translation": "Nije moguće napustiti kanal izravne poruke." + }, + { + "id": "api.channel.leave.default.app_error", + "translation": "Nije moguće napustiti standardni kanal {{.Channel}}." + }, + { + "id": "api.channel.join_channel.post_and_forget", + "translation": "%v se pridružio/la kanalu." + }, + { + "id": "api.channel.join_channel.permissions.app_error", + "translation": "Nemaš odgovarajuće dozvole." + }, + { + "id": "api.channel.guest_join_channel.post_and_forget", + "translation": "%v se pridružio/la kanalu kao gost." + }, + { + "id": "api.channel.get_channel_moderations.license.error", + "translation": "Tvoja licenca ne podržava moderiranje kanala" + }, + { + "id": "api.channel.delete_channel.type.invalid", + "translation": "Nije moguće izbrisati kanale izravne ili grupne poruke" + }, + { + "id": "api.channel.delete_channel.deleted.app_error", + "translation": "Kanal je arhiviran ili izbrisan." + }, + { + "id": "api.channel.delete_channel.cannot.app_error", + "translation": "Nije moguće izbrisati standardni kanal {{.Channel}}." + }, + { + "id": "api.channel.delete_channel.archived", + "translation": "%v je arhivirao/la kanal." + }, + { + "id": "api.channel.create_group.bad_user.app_error", + "translation": "Jedan od navedenih korisnika ne postoji." + }, + { + "id": "api.channel.create_group.bad_size.app_error", + "translation": "Kanali grupnih poruka moraju sadržavati najmanje 3 i ne više od 8 korisnika." + }, + { + "id": "api.channel.create_direct_channel.invalid_user.app_error", + "translation": "Neispravan korisnički ID za izravnu izradu kanala." + }, + { + "id": "api.channel.create_channel.max_channel_limit.app_error", + "translation": "Nije moguće stvoriti više od {{.MaxChannelsPerTeam}} kanala za trenutačni tim." + }, + { + "id": "api.channel.channel_member_counts_by_group.license.error", + "translation": "Tvoja licenca ne podržava grupe" + }, + { + "id": "api.channel.change_channel_privacy.public_to_private", + "translation": "Ovaj kanal je pretvoren u privatni kanal." + }, + { + "id": "api.channel.change_channel_privacy.private_to_public", + "translation": "Ovaj je kanal pretvoren u javni kanal i može mu se pridružiti bilo koji član tima." + }, + { + "id": "api.channel.add_user_to_channel.type.app_error", + "translation": "Nije moguće dodati korisnika ovoj vrsti kanala." + }, + { + "id": "api.channel.add_user.to.channel.failed.deleted.app_error", + "translation": "Dodavanje korisnika na kanal nije uspjelo jer je uklonjen iz tima." + }, + { + "id": "api.channel.add_members.user_denied", + "translation": "Članstvo u kanalu je odbijeno sljedećim korisnicima zbog ograničenja grupe: {{ .UserIDs }}" + }, + { + "id": "api.bot.teams_channels.add_message_mobile", + "translation": "Dodaj me timovima i kanalima u kojima želiš da interagiram. Za to koristi preglednik ili aplikaciju Mattermost Desktop." + }, + { + "id": "api.channel.create_channel.direct_channel.app_error", + "translation": "Mora koristiti API uslugu createDirectChannel za stvaranje izravnog kanala za poruke." + }, + { + "id": "api.channel.create_channel.direct_channel.team_restricted_error", + "translation": "Izravni kanal se ne može stvoriti između ovih korisnika jer ne dijele zajednički tim." + }, + { + "id": "api.acknowledgement.delete.deadline.app_error", + "translation": "Ne možeš ukloniti potvrdu nakon što prođe 5 minuta." + }, + { + "id": "api.acknowledgement.delete.archived_channel.app_error", + "translation": "Ne možeš ukloniti potvrdu u arhiviranom kanalu." + }, + { + "id": "api.channel.create_default_channels.off_topic", + "translation": "Van teme" + }, + { + "id": "api.admin.syncables_error", + "translation": "nije uspio/uspjela dodati korisnika timovima grupe i kanalima grupe" + }, + { + "id": "api.email.send_warn_metric_ack.failure.app_error", + "translation": "Neuspjelo slanje administratorske e-mail poruke potvrde" + }, + { + "id": "api.channel.create_default_channels.town_square", + "translation": "Gradski trg" + }, + { + "id": "api.channel.patch_channel_moderations.cache_invalidation.error", + "translation": "Greška u poništavanju predmemorije" + } +] diff --git a/server/i18n/hu.json b/server/i18n/hu.json index b9c46aa49bc..4002b4c42a2 100644 --- a/server/i18n/hu.json +++ b/server/i18n/hu.json @@ -1855,10 +1855,6 @@ "id": "model.file_info.is_valid.create_at.app_error", "translation": "Érvénytelen érték: create_at." }, - { - "id": "model.file_info.get.gif.app_error", - "translation": "Nem sikerült dekódolni a gif-et." - }, { "id": "model.emoji.user_id.app_error", "translation": "Érvénytelen létrehozó azonosító." @@ -9119,10 +9115,6 @@ "id": "api.file.cloud_upload.app_error", "translation": "Az mmctl segítségével történő feltöltés egy felhő alapú példányra nem támogatott. Kérjük, tekintse meg a dokumentációt itt: https://docs.mattermost.com/manage/cloud-data-export.html." }, - { - "id": "app.recent_searches.app_error", - "translation": "Hiba a legutóbbi keresések letöltése közben" - }, { "id": "app.usage.get_storage_usage.app_error", "translation": "Hiba a tároló használtságának lekérdezése közben." @@ -9147,26 +9139,10 @@ "id": "model.config.is_valid.image_decoder_concurrency.app_error", "translation": "Érvénytelen dekódoló párhuzamosság {{.Value}}. Pozitív számnak vagy -1-nek kell lennie." }, - { - "id": "app.post.get_top_threads_for_team_since.app_error", - "translation": "A csapat legnépszerűbb szálai nem érhetők el." - }, - { - "id": "app.post.get_top_threads_for_user_since.app_error", - "translation": "A felhasználó legnépszerűbb szálai nem érhetők el." - }, - { - "id": "app.insights.feature_disabled", - "translation": "Áttekintések szolgáltatás le van tiltva." - }, { "id": "app.post.cloud.get.app_error", "translation": "Nem lehetséges a bejegyzés lekérése, mivel a felhő csomagjának korlátján túl van." }, - { - "id": "api.insights.feature_disabled", - "translation": "Áttekintések egy olyan funkció jelölő mögött van, ami nincs engedélyezve." - }, { "id": "app.last_accessible_post.app_error", "translation": "Hiba történt a legutóbbi elérhető bejegyzés lekérése közben" @@ -9195,10 +9171,6 @@ "id": "app.team.clear_cache.app_error", "translation": "Hiba a csapattagok gyorsítótárának törlésében" }, - { - "id": "app.post.get_top_dms_for_user_since.app_error", - "translation": "Nem lehet lekérni a felhasználó legtöbb közvetlen üzeneteit." - }, { "id": "api.command_help.success", "translation": "A Mattermost egy nyílt forráskódú platform a biztonságos kommunikációhoz, együttműködéshez és az eszközök és csapatok közötti munka összehangolásához.\nA Mattermost három kulcsfontosságú eszközt tartalmaz:\n\n**Csatornák** - Maradjon kapcsolatban csapatával 1:1 és csoportos üzenetküldéssel.\n**[Forgatókönyvek](/playbooks)** - Építsen és konfiguráljon megismételhető folyamatokat meghatározott és kiszámítható eredmények elérése érdekében.\n**[Táblák](/boards)** - Kezelje a projekteket és feladatokat Kanban tábla struktúrában, hogy segítse csapatát a kulcsfontosságú mérföldkövek elérésében.\n\n[Dokumentáció és útmutatók megtekintése]({{.HelpLink}})" diff --git a/server/i18n/id.json b/server/i18n/id.json new file mode 100644 index 00000000000..0fe842e3802 --- /dev/null +++ b/server/i18n/id.json @@ -0,0 +1,2952 @@ +[ + { + "id": "store.sql_user.get_for_login.app_error", + "translation": "Kami tidak dapat menemukan akun yang telah ada yang sesuai dengan email Anda untuk tim ini. Tim ini mungkin membutuhkan undangan dari pemilik tim untuk bergabung." + }, + { + "id": "app.scheme.save.app_error", + "translation": "Tidak dapat memuat kiriman" + }, + { + "id": "app.scheme.get.app_error", + "translation": "Tidak dapat memperoleh kanal" + }, + { + "id": "app.reaction.save.save.app_error", + "translation": "Tidak dapat memuat kiriman" + }, + { + "id": "app.emoji.delete.app_error", + "translation": "Tidak dapat memuat kiriman" + }, + { + "id": "store.sql_command.save.get.app_error", + "translation": "Tidak dapat memuat kiriman" + }, + { + "id": "app.channel.update.bad_id", + "translation": "Tidak dapat memperoleh kanal" + }, + { + "id": "store.sql_channel.save.direct_channel.app_error", + "translation": "Gunakan SaveDirectChannel untuk membuat kanal langsung" + }, + { + "id": "app.channel.get_deleted.existing.app_error", + "translation": "Tidak dapat menemukan kanal" + }, + { + "id": "app.channel.get_channels.not_found.app_error", + "translation": "Tidak ada kanal ditemukan" + }, + { + "id": "app.channel.get_by_name.missing.app_error", + "translation": "Kanal tidak ada" + }, + { + "id": "store.sql_channel.get.existing.app_error", + "translation": "Tidak dapat menemukan kanal" + }, + { + "id": "app.audit.save.saving.app_error", + "translation": "Kami mengalami masalah saat mencari akun" + }, + { + "id": "app.audit.permanent_delete_by_user.app_error", + "translation": "Kami mengalami masalah saat mencari akun" + }, + { + "id": "app.audit.get.finding.app_error", + "translation": "Kami mengalami masalah saat mencari akun" + }, + { + "id": "plugin_api.get_file_link.no_post.app_error", + "translation": "Tidak dapat memperoleh tautan publik untuk data. Data harus dilampirkan pada postingan yang bisa dibaca oleh pengguna saat ini." + }, + { + "id": "plugin_api.get_file_link.disabled.app_error", + "translation": "Tautan publik telah dinonaktifkan." + }, + { + "id": "model.utils.decode_json.app_error", + "translation": "tidak dapat membaca kode" + }, + { + "id": "model.user_access_token.is_valid.user_id.app_error", + "translation": "ID pengguna tidak valid" + }, + { + "id": "model.user_access_token.is_valid.token.app_error", + "translation": "Token akses tidak valid" + }, + { + "id": "model.user.is_valid.update_at.app_error", + "translation": "Pembaruan harus pada waktu yang valid" + }, + { + "id": "model.user.is_valid.pwd_uppercase_symbol.app_error", + "translation": "Kata sandi Anda harus berjumlah paling sedikit {{.Min}} karakter, dan harus terdapat minimal satu huruf kecil." + }, + { + "id": "model.user.is_valid.pwd_uppercase_number_symbol.app_error", + "translation": "Kata sandi Anda harus berjumlah paling sedikit {{.Min}} karakter, dan harus terdapat minimal satu huruf kecil." + }, + { + "id": "model.user.is_valid.pwd_uppercase_number.app_error", + "translation": "Kata sandi Anda harus berjumlah paling sedikit {{.Min}} karakter, dan harus terdapat minimal satu huruf kecil." + }, + { + "id": "model.user.is_valid.pwd_uppercase.app_error", + "translation": "Kata sandi Anda harus berjumlah paling sedikit {{.Min}} karakter, dan harus terdapat minimal satu huruf kecil." + }, + { + "id": "model.user.is_valid.pwd_symbol.app_error", + "translation": "Kata sandi Anda harus berjumlah paling sedikit {{.Min}} karakter, dan harus terdapat minimal satu huruf kecil." + }, + { + "id": "model.user.is_valid.pwd_number_symbol.app_error", + "translation": "Kata sandi Anda harus berjumlah paling sedikit {{.Min}} karakter, dan harus terdapat minimal satu huruf kecil." + }, + { + "id": "model.user.is_valid.pwd_number.app_error", + "translation": "Kata sandi Anda harus berjumlah paling sedikit {{.Min}} karakter, dan harus terdapat minimal satu huruf kecil." + }, + { + "id": "model.user.is_valid.pwd_lowercase_uppercase_symbol.app_error", + "translation": "Kata sandi Anda harus berjumlah paling sedikit {{.Min}} karakter, dan harus terdapat minimal satu huruf kecil." + }, + { + "id": "model.user.is_valid.pwd_lowercase_uppercase_number_symbol.app_error", + "translation": "Kata sandi Anda harus berjumlah paling sedikit {{.Min}} karakter, dan harus terdapat minimal satu huruf kecil." + }, + { + "id": "model.user.is_valid.pwd_lowercase_uppercase_number.app_error", + "translation": "Kata sandi Anda harus berjumlah paling sedikit {{.Min}} karakter, dan harus terdapat minimal satu huruf kecil." + }, + { + "id": "model.post.is_valid.create_at.app_error", + "translation": "Pembuatan harus saat waktu yang valid" + }, + { + "id": "model.post.is_valid.channel_id.app_error", + "translation": "ID Kanal tidak valid" + }, + { + "id": "model.outgoing_hook.username.app_error", + "translation": "Nama tidak valid" + }, + { + "id": "model.outgoing_hook.is_valid.words.app_error", + "translation": "Kata pemicu tidak valid" + }, + { + "id": "model.outgoing_hook.is_valid.user_id.app_error", + "translation": "ID pengguna tidak valid" + }, + { + "id": "model.outgoing_hook.is_valid.url.app_error", + "translation": "URL callback tidak valid. Harus berupa URL yang valid dan dimulai dengan http:// atau https://" + }, + { + "id": "model.outgoing_hook.is_valid.update_at.app_error", + "translation": "Pembaruan harus pada waktu yang valid" + }, + { + "id": "model.outgoing_hook.is_valid.trigger_words.app_error", + "translation": "Kata pemicu tidak valid" + }, + { + "id": "model.outgoing_hook.is_valid.token.app_error", + "translation": "Token tidak valid" + }, + { + "id": "model.outgoing_hook.is_valid.team_id.app_error", + "translation": "ID tim tidak valid" + }, + { + "id": "model.outgoing_hook.is_valid.id.app_error", + "translation": "ID tidak valid" + }, + { + "id": "model.outgoing_hook.is_valid.display_name.app_error", + "translation": "Tipe tidak valid" + }, + { + "id": "model.outgoing_hook.is_valid.description.app_error", + "translation": "Deskripsi tidak valid" + }, + { + "id": "model.outgoing_hook.is_valid.create_at.app_error", + "translation": "Pembuatan harus saat waktu yang valid" + }, + { + "id": "model.outgoing_hook.is_valid.channel_id.app_error", + "translation": "ID Kanal tidak valid" + }, + { + "id": "model.outgoing_hook.is_valid.callback.app_error", + "translation": "URL callback tidak valid" + }, + { + "id": "model.outgoing_hook.icon_url.app_error", + "translation": "Jangkauan tidak valid" + }, + { + "id": "model.oauth.is_valid.update_at.app_error", + "translation": "Pembaruan harus pada waktu yang valid" + }, + { + "id": "model.oauth.is_valid.name.app_error", + "translation": "Nama tidak valid" + }, + { + "id": "model.oauth.is_valid.icon_url.app_error", + "translation": "URL SSO SAML harus berupa URL yang valid dan dimulai dengan http:// atau https://" + }, + { + "id": "model.oauth.is_valid.homepage.app_error", + "translation": "URL SSO SAML harus berupa URL yang valid dan dimulai dengan http:// atau https://" + }, + { + "id": "model.oauth.is_valid.description.app_error", + "translation": "Deskripsi tidak valid" + }, + { + "id": "model.oauth.is_valid.creator_id.app_error", + "translation": "ID pembuat tidak valid" + }, + { + "id": "model.oauth.is_valid.create_at.app_error", + "translation": "Pembuatan harus saat waktu yang valid" + }, + { + "id": "model.oauth.is_valid.client_secret.app_error", + "translation": "Secret klien tidak valid" + }, + { + "id": "model.oauth.is_valid.callback.app_error", + "translation": "URL SSO SAML harus berupa URL yang valid dan dimulai dengan http:// atau https://" + }, + { + "id": "model.oauth.is_valid.app_id.app_error", + "translation": "ID aplikasi tidak valid" + }, + { + "id": "model.job.is_valid.type.app_error", + "translation": "Tipe tidak valid" + }, + { + "id": "model.job.is_valid.id.app_error", + "translation": "Tipe tidak valid" + }, + { + "id": "model.job.is_valid.create_at.app_error", + "translation": "Pembuatan harus saat waktu yang valid" + }, + { + "id": "model.incoming_hook.username.app_error", + "translation": "Nama tidak valid" + }, + { + "id": "model.incoming_hook.user_id.app_error", + "translation": "ID pengguna tidak valid" + }, + { + "id": "model.incoming_hook.update_at.app_error", + "translation": "Pembaruan harus pada waktu yang valid" + }, + { + "id": "model.incoming_hook.team_id.app_error", + "translation": "ID tim tidak valid" + }, + { + "id": "model.incoming_hook.parse_data.app_error", + "translation": "Tidak dapat menangani data yang masuk" + }, + { + "id": "model.incoming_hook.id.app_error", + "translation": "ID tidak valid" + }, + { + "id": "model.incoming_hook.icon_url.app_error", + "translation": "ID root tidak valid" + }, + { + "id": "model.incoming_hook.display_name.app_error", + "translation": "Tipe tidak valid" + }, + { + "id": "model.incoming_hook.description.app_error", + "translation": "Deskripsi tidak valid" + }, + { + "id": "model.incoming_hook.create_at.app_error", + "translation": "Pembuatan harus saat waktu yang valid" + }, + { + "id": "model.incoming_hook.channel_id.app_error", + "translation": "ID Kanal tidak valid" + }, + { + "id": "model.guest.is_valid.emails.app_error", + "translation": "Email tidak valid" + }, + { + "id": "model.guest.is_valid.email.app_error", + "translation": "Email tidak valid" + }, + { + "id": "model.guest.is_valid.channels.app_error", + "translation": "ID Kanal tidak valid" + }, + { + "id": "model.guest.is_valid.channel.app_error", + "translation": "ID Kanal tidak valid" + }, + { + "id": "model.file_info.get.gif.app_error", + "translation": "Tidak dapat membaca kode gif." + }, + { + "id": "model.emoji.user_id.app_error", + "translation": "ID pembuat tidak valid" + }, + { + "id": "model.emoji.update_at.app_error", + "translation": "Pembaruan harus pada waktu yang valid" + }, + { + "id": "model.emoji.name.app_error", + "translation": "Nama harus berupa 1 hingga 64 huruf kecil karakter alfanumerik " + }, + { + "id": "model.emoji.id.app_error", + "translation": "ID emoji tidak valid" + }, + { + "id": "model.emoji.create_at.app_error", + "translation": "Pembuatan harus saat waktu yang valid" + }, + { + "id": "model.config.is_valid.websocket_url.app_error", + "translation": "URL SSO SAML harus berupa URL yang valid dan dimulai dengan http:// atau https://" + }, + { + "id": "model.config.is_valid.sql_query_timeout.app_error", + "translation": "Tinggi gambar profil untuk pengaturan berkas tidak valid. Harus berupa angka positif." + }, + { + "id": "model.config.is_valid.sql_max_conn.app_error", + "translation": "Jumlah koneksi terbuka untuk pengaturan SQL tidak valid. Harus berupa angka positif." + }, + { + "id": "model.config.is_valid.sql_idle.app_error", + "translation": "Jumlah koneksi tidak jalan untuk pengaturan SQL tidak valid. Harus berupa angka positif." + }, + { + "id": "model.config.is_valid.sql_driver.app_error", + "translation": "Driver name untuk pengaturan SQL tidak valid. Harus berupa 'mysql' atau 'postgres'" + }, + { + "id": "model.config.is_valid.sql_data_src.app_error", + "translation": "Sumber data untuk pengaturan SQL tidak valid. Wajib diisi." + }, + { + "id": "model.config.is_valid.sitename_length.app_error", + "translation": "Nama situs harus kurang atau sama dengan {{.MaxLength}} karakter." + }, + { + "id": "model.config.is_valid.site_url.app_error", + "translation": "URL SSO SAML harus berupa URL yang valid dan dimulai dengan http:// atau https://" + }, + { + "id": "model.config.is_valid.saml_username_attribute.app_error", + "translation": "Atribut Nama Pengguna tidak valid. Wajib diisi." + }, + { + "id": "model.config.is_valid.saml_public_cert.app_error", + "translation": "Sertifikat Publik Service Provider tidak ditemukan. Apakah Anda belum mengunggahnya?" + }, + { + "id": "model.config.is_valid.saml_private_key.app_error", + "translation": "Private Key Service Provider tidak ditemukan. Apakah Anda belum mengunggahnya?" + }, + { + "id": "model.config.is_valid.saml_idp_url.app_error", + "translation": "URL SSO SAML harus berupa URL yang valid dan dimulai dengan http:// atau https://" + }, + { + "id": "model.config.is_valid.saml_idp_descriptor_url.app_error", + "translation": "URL Issuer Identity Provider harus berupa URL yang valid dan dimulai dengan http:// atau https://" + }, + { + "id": "model.config.is_valid.saml_idp_cert.app_error", + "translation": "Sertifikat Publik Identity Provider tidak ditemukan. Apakah Anda belum mengunggahnya?" + }, + { + "id": "model.config.is_valid.saml_email_attribute.app_error", + "translation": "Atribut email tidak valid. Wajib diisi." + }, + { + "id": "model.config.is_valid.saml_assertion_consumer_service_url.app_error", + "translation": "URL Login Service Provider harus berupa URL yang valid dan dimulai dengan http:// atau https://" + }, + { + "id": "model.config.is_valid.restrict_direct_message.app_error", + "translation": "Pembatasan pesan langsung tidak valid. Harus berupa 'siapa saja', atau 'tim'" + }, + { + "id": "model.config.is_valid.rate_sec.app_error", + "translation": "Pengaturan limit per detik tidak valid. Harus berupa angka positif." + }, + { + "id": "model.config.is_valid.rate_mem.app_error", + "translation": "Ukuran memori yang disimpan untuk pengaturan limit tidak valid. Harus berupa angka positif." + }, + { + "id": "model.config.is_valid.password_length.app_error", + "translation": "Panjang kata sandi paling sedikit harus lebih besar atau sama dengan {{.MinLength}} dan lebih kecil atau sama dengan {{.MaxLength}}." + }, + { + "id": "model.config.is_valid.max_users.app_error", + "translation": "Jumlah pengguna maksimum per tim untuk pengaturan tim tidak valid. Harus berupa angka positif." + }, + { + "id": "model.config.is_valid.max_notify_per_channel.app_error", + "translation": "Jumlah pengguna maksimum per tim untuk pengaturan tim tidak valid. Harus berupa angka positif." + }, + { + "id": "model.config.is_valid.max_channels.app_error", + "translation": "Jumlah pengguna maksimum per tim untuk pengaturan tim tidak valid. Harus berupa angka positif." + }, + { + "id": "model.config.is_valid.login_attempts.app_error", + "translation": "Nilai maksimum percobaan masuk untuk pengaturan layanan tidak valid. Harus berupa angka positif." + }, + { + "id": "model.config.is_valid.listen_address.app_error", + "translation": "Alamat \"listen\" untuk pengaturan layanan tidak valid. Kolom ini wajib diisi." + }, + { + "id": "web.incoming_webhook.user.app_error", + "translation": "Tidak dapat menemukan pengguna" + }, + { + "id": "web.incoming_webhook.text.app_error", + "translation": "Tidak ada teks yang ditetapkan" + }, + { + "id": "web.incoming_webhook.permissions.app_error", + "translation": "Hak akses kanal tidak sesuai" + }, + { + "id": "web.incoming_webhook.parse.app_error", + "translation": "Tidak dapat menangani data yang masuk" + }, + { + "id": "web.incoming_webhook.invalid.app_error", + "translation": "Metode tidak valid" + }, + { + "id": "web.incoming_webhook.disabled.app_error", + "translation": "Perintah telah dinonaktifkan oleh admin sistem." + }, + { + "id": "web.incoming_webhook.channel.app_error", + "translation": "Tidak dapat menemukan kanal" + }, + { + "id": "web.command_webhook.parse.app_error", + "translation": "Tidak dapat menangani data yang masuk" + }, + { + "id": "web.command_webhook.command.app_error", + "translation": "Tidak dapat menemukan kanal" + }, + { + "id": "model.config.is_valid.ldap_username", + "translation": "Kolom \"Atribut Nama Pengguna\" LDAP wajib diisi." + }, + { + "id": "model.config.is_valid.ldap_sync_interval.app_error", + "translation": "Waktu interval sinkronisasi tidak valid. Waktu paling sedikit satu menit." + }, + { + "id": "model.config.is_valid.ldap_server", + "translation": "Kolom \"Server LDAP\" LDAP wajib diisi." + }, + { + "id": "model.config.is_valid.ldap_security.app_error", + "translation": "Keamanan koneksi untuk pengaturan LDAP tidak valid. Harus berupa '', 'TLS', atau 'STARTTLS'" + }, + { + "id": "model.config.is_valid.ldap_max_page_size.app_error", + "translation": "Ukuran maksimum halaman tidak valid." + }, + { + "id": "model.config.is_valid.ldap_login_id", + "translation": "Kolom \"Atribut ID\" LDAP wajib diisi." + }, + { + "id": "model.config.is_valid.ldap_id", + "translation": "Kolom \"Atribut ID\" LDAP wajib diisi." + }, + { + "id": "model.config.is_valid.ldap_email", + "translation": "Kolom \"Atribut Email\" LDAP wajib diisi." + }, + { + "id": "model.config.is_valid.ldap_basedn", + "translation": "Kolom \"BaseDN\" LDAP wajib diisi." + }, + { + "id": "model.config.is_valid.file_salt.app_error", + "translation": "Salt tautan publik untuk pengaturan berkas tidak valid. Karakter harus berjumlah 32 atau lebih." + }, + { + "id": "model.config.is_valid.file_driver.app_error", + "translation": "Nama driver untuk pengaturan berkas tidak valid. Harus berupa 'local' atau 'amazonS3'" + }, + { + "id": "model.config.is_valid.encrypt_sql.app_error", + "translation": "Kunci enkripsi untuk pengaturan SQL tidak valid. Karakter harus berjumlah 32 atau lebih." + }, + { + "id": "model.config.is_valid.email_security.app_error", + "translation": "Pengaturan keamanan sambungan untuk email tidak valid. Harus berupa \",'TLS', atau 'STARTTLS'" + }, + { + "id": "model.compliance.is_valid.start_end_at.app_error", + "translation": "Harus lebih besar dari" + }, + { + "id": "model.compliance.is_valid.start_at.app_error", + "translation": "Harus berupa waktu yang valid" + }, + { + "id": "model.compliance.is_valid.id.app_error", + "translation": "ID tidak valid" + }, + { + "id": "model.compliance.is_valid.end_at.app_error", + "translation": "Harus berupa waktu yang valid" + }, + { + "id": "model.compliance.is_valid.desc.app_error", + "translation": "Deskripsi tidak valid" + }, + { + "id": "model.compliance.is_valid.create_at.app_error", + "translation": "Pembuatan harus saat waktu yang valid" + }, + { + "id": "model.command_hook.user_id.app_error", + "translation": "ID pengguna tidak valid" + }, + { + "id": "model.command_hook.root_id.app_error", + "translation": "ID root tidak valid" + }, + { + "id": "model.command_hook.create_at.app_error", + "translation": "Pembuatan harus saat waktu yang valid" + }, + { + "id": "model.command_hook.channel_id.app_error", + "translation": "ID Kanal tidak valid" + }, + { + "id": "model.command.is_valid.user_id.app_error", + "translation": "ID pengguna tidak valid" + }, + { + "id": "model.command.is_valid.url_http.app_error", + "translation": "URL tidak valid. Alamat URL harus valid dan dimulai dengan http:// atau https://" + }, + { + "id": "model.command.is_valid.url.app_error", + "translation": "ID tidak valid" + }, + { + "id": "model.command.is_valid.update_at.app_error", + "translation": "Pembaruan harus pada waktu yang valid" + }, + { + "id": "model.command.is_valid.trigger.app_error", + "translation": "Trigger tidak valid" + }, + { + "id": "model.command.is_valid.token.app_error", + "translation": "Token tidak valid" + }, + { + "id": "model.command.is_valid.team_id.app_error", + "translation": "ID tim tidak valid" + }, + { + "id": "model.command.is_valid.method.app_error", + "translation": "Metode tidak valid" + }, + { + "id": "model.command.is_valid.id.app_error", + "translation": "ID tidak valid" + }, + { + "id": "model.command.is_valid.display_name.app_error", + "translation": "Tipe tidak valid" + }, + { + "id": "model.command.is_valid.description.app_error", + "translation": "Deskripsi tidak valid" + }, + { + "id": "model.command.is_valid.create_at.app_error", + "translation": "Pembuatan harus saat waktu yang valid" + }, + { + "id": "model.cluster.is_valid.id.app_error", + "translation": "ID tidak valid" + }, + { + "id": "model.channel_member.is_valid.user_id.app_error", + "translation": "ID pengguna tidak valid" + }, + { + "id": "model.channel_member.is_valid.unread_level.app_error", + "translation": "Level tanda belum dibaca tidak valid" + }, + { + "id": "model.channel_member.is_valid.notify_level.app_error", + "translation": "Level notifikasi tidak valid" + }, + { + "id": "model.channel_member.is_valid.channel_id.app_error", + "translation": "ID Kanal tidak valid" + }, + { + "id": "model.channel.is_valid.update_at.app_error", + "translation": "Pembaruan harus pada waktu yang valid" + }, + { + "id": "model.channel.is_valid.type.app_error", + "translation": "Tipe tidak valid" + }, + { + "id": "model.channel.is_valid.purpose.app_error", + "translation": "Prop tidak valid" + }, + { + "id": "model.channel.is_valid.name.app_error", + "translation": "Nama saluran tidak valid. Id pengguna tidak diizinkan dalam nama saluran (channel name) untuk saluran pesan tidak langsung (non-direct message channels)." + }, + { + "id": "model.channel.is_valid.id.app_error", + "translation": "ID tidak valid" + }, + { + "id": "model.channel.is_valid.header.app_error", + "translation": "Judul tidak valid" + }, + { + "id": "model.channel.is_valid.display_name.app_error", + "translation": "Tampilan nama tidak valid" + }, + { + "id": "model.channel.is_valid.creator_id.app_error", + "translation": "ID pembuat tidak valid" + }, + { + "id": "model.channel.is_valid.create_at.app_error", + "translation": "Pembuatan harus saat waktu yang valid" + }, + { + "id": "model.bot.is_valid.username.app_error", + "translation": "Nama tidak valid" + }, + { + "id": "model.bot.is_valid.user_id.app_error", + "translation": "ID pengguna tidak valid" + }, + { + "id": "model.bot.is_valid.update_at.app_error", + "translation": "ID pembuat tidak valid" + }, + { + "id": "model.bot.is_valid.description.app_error", + "translation": "Deskripsi tidak valid" + }, + { + "id": "model.bot.is_valid.creator_id.app_error", + "translation": "ID pembuat tidak valid" + }, + { + "id": "model.bot.is_valid.create_at.app_error", + "translation": "ID pembuat tidak valid" + }, + { + "id": "model.authorize.is_valid.user_id.app_error", + "translation": "ID pengguna tidak valid" + }, + { + "id": "model.authorize.is_valid.state.app_error", + "translation": "Keadaan tidak valid" + }, + { + "id": "model.authorize.is_valid.scope.app_error", + "translation": "Jangkauan tidak valid" + }, + { + "id": "model.authorize.is_valid.redirect_uri.app_error", + "translation": "URI pengalihan tidak valid" + }, + { + "id": "model.authorize.is_valid.expires.app_error", + "translation": "Waktu kedaluwarsa harus diatur" + }, + { + "id": "model.authorize.is_valid.create_at.app_error", + "translation": "Pembuatan harus saat waktu yang valid" + }, + { + "id": "model.authorize.is_valid.client_id.app_error", + "translation": "ID klien tidak valid" + }, + { + "id": "model.authorize.is_valid.auth_code.app_error", + "translation": "Kode otorisasi tidak valid" + }, + { + "id": "model.access.is_valid.user_id.app_error", + "translation": "ID pengguna tidak valid" + }, + { + "id": "model.access.is_valid.refresh_token.app_error", + "translation": "Segarkan token tidak valid" + }, + { + "id": "model.access.is_valid.redirect_uri.app_error", + "translation": "URI pengalihan tidak valid" + }, + { + "id": "model.access.is_valid.client_id.app_error", + "translation": "ID klien tidak valid" + }, + { + "id": "model.access.is_valid.access_token.app_error", + "translation": "Token akses tidak valid" + }, + { + "id": "mfa.validate_token.authenticate.app_error", + "translation": "Token MFA tidak valid." + }, + { + "id": "mfa.mfa_disabled.app_error", + "translation": "Autentikasi multi faktor diperlukan pada server ini." + }, + { + "id": "mfa.generate_qr_code.create_code.app_error", + "translation": "Terjadi kesalahan saat membuat QR Code" + }, + { + "id": "mfa.activate.bad_token.app_error", + "translation": "Token MFA tidak valid." + }, + { + "id": "mattermost.bulletin.subject", + "translation": "Buletin Keamanan Mattermost" + }, + { + "id": "manaultesting.test_autolink.unable.app_error", + "translation": "Tidak dapat memperoleh kanal" + }, + { + "id": "manaultesting.manual_test.parse.app_error", + "translation": "Tidak data meneruskan URL" + }, + { + "id": "ent.saml.service_disable.app_error", + "translation": "SAML tidak diatur atau didukung oleh server ini" + }, + { + "id": "ent.saml.metadata.app_error", + "translation": "Terjadi kesalahan saat membuat Metadata Service Provider." + }, + { + "id": "ent.saml.license_disable.app_error", + "translation": "Lisensi Anda tidak mendukung autentikasi SAML." + }, + { + "id": "ent.saml.do_login.validate.app_error", + "translation": "Terjadi kesalahan saat mengesahkan balasan dari Identity Provider. Silakan hubungi Administrator Sistem Anda." + }, + { + "id": "ent.saml.do_login.parse.app_error", + "translation": "Terjadi kesalahan saat menerima balasan dari Identity Provider. Silakan hubungi Administrator Sistem Anda." + }, + { + "id": "ent.saml.do_login.empty_response.app_error", + "translation": "Kami menerima balasan kosong dari Identity Provider" + }, + { + "id": "ent.saml.do_login.decrypt.app_error", + "translation": "Masuk dengan SAML gagal karena terjadi kesalahan saat mendekripsi balasan dari Identity Provider. Silakan hubungi Administrator Sistem Anda." + }, + { + "id": "ent.saml.configure.not_encrypted_response.app_error", + "translation": "Masuk dengan SAML gagal karena balasan Identity Provider tidak terenkripsi. Silakan hubungi Administrator Sistem Anda." + }, + { + "id": "ent.saml.configure.load_private_key.app_error", + "translation": "Masuk dengan SAML gagal karena Kunci Privat Service Provider tidak ditemukan. Silakan hubungi Administrator Sistem Anda." + }, + { + "id": "ent.saml.configure.load_idp_cert.app_error", + "translation": "Berkas Sertifikat Publik Identity Provider tidak dapat ditemukan. Silakan hubungi Administrator Sistem Anda." + }, + { + "id": "ent.saml.configure.encryption_not_enabled.app_error", + "translation": "Masuk dengan SAML gagal karena enkripsi tidak diaktifkan. Silakan hubungi Administrator Sistem Anda." + }, + { + "id": "ent.saml.build_request.encoding.app_error", + "translation": "Terjadi kesalahan saat membaca kode permintaan untuk Identity Provider. Silakan hubungi Administrator Sistem." + }, + { + "id": "ent.saml.build_request.app_error", + "translation": "Terjadi kesalahan saat memulai permintaan ke Identity Provider. Silakan hubungi Administrator Sistem." + }, + { + "id": "ent.saml.attribute.app_error", + "translation": "Masuk dengan SAML gagal karena enkripsi tidak diaktifkan. Silakan hubungi Administrator Sistem Anda." + }, + { + "id": "ent.message_export.global_relay.create_file_in_zip.app_error", + "translation": "Tidak dapat memuat kiriman" + }, + { + "id": "ent.message_export.global_relay.close_zip_file.app_error", + "translation": "Tidak dapat memuat kiriman" + }, + { + "id": "ent.ldap.validate_guest_filter.app_error", + "translation": "Filter LDAP tidak valid" + }, + { + "id": "ent.ldap.validate_filter.app_error", + "translation": "Filter LDAP tidak valid" + }, + { + "id": "ent.ldap.validate_admin_filter.app_error", + "translation": "Filter LDAP tidak valid" + }, + { + "id": "ent.ldap.syncronize.get_all.app_error", + "translation": "Tidak dapat mendapatkan data semua pengguna dengan LDAP" + }, + { + "id": "ent.ldap.do_login.user_not_registered.app_error", + "translation": "Pengguna tidak terdaftar pada server LDAP" + }, + { + "id": "ent.ldap.do_login.user_filtered.app_error", + "translation": "Akun LDAP Anda tidak memiliki hak akses untuk menggunakan server Mattermost ini. Silakan hubungi Administrator Sistem untuk memeriksa filter LDAP pengguna." + }, + { + "id": "ent.ldap.do_login.unable_to_connect.app_error", + "translation": "Gagal terhubung ke server LDAP" + }, + { + "id": "ent.ldap.do_login.search_ldap_server.app_error", + "translation": "Gagal mencari server LDAP" + }, + { + "id": "ent.ldap.do_login.matched_to_many_users.app_error", + "translation": "Nama pengguna yang diberikan cocok dengan banyak pengguna" + }, + { + "id": "ent.ldap.do_login.licence_disable.app_error", + "translation": "Fungsi LDAP dinonaktifkan oleh lisensi saat ini. Silakan hubungi administrator sistem untuk meningkatkan lisensi enterprise Anda." + }, + { + "id": "ent.ldap.do_login.invalid_password.app_error", + "translation": "Kata sandi tidak valid" + }, + { + "id": "ent.ldap.do_login.bind_admin_user.app_error", + "translation": "Tidak dapat menghubungi server LDAP. Periksa BindUsername dan BindPassword." + }, + { + "id": "ent.elasticsearch.index_user.error", + "translation": "Tidak dapat memuat kiriman" + }, + { + "id": "ent.elasticsearch.index_post.error", + "translation": "Tidak dapat memuat kiriman" + }, + { + "id": "ent.elasticsearch.index_channel.error", + "translation": "Tidak dapat memperoleh kanal" + }, + { + "id": "ent.elasticsearch.delete_user.error", + "translation": "Tidak dapat memuat kiriman" + }, + { + "id": "ent.elasticsearch.delete_post.error", + "translation": "Tidak dapat memuat kiriman" + }, + { + "id": "ent.elasticsearch.delete_channel.error", + "translation": "Tidak dapat memperoleh kanal" + }, + { + "id": "ent.data_retention.generic.license.error", + "translation": "Lisensi Anda tidak mendukung autentikasi SAML." + }, + { + "id": "ent.compliance.licence_disable.app_error", + "translation": "Fungsi LDAP dinonaktifkan oleh lisensi saat ini. Silakan hubungi administrator sistem untuk meningkatkan lisensi enterprise Anda." + }, + { + "id": "ent.compliance.csv.zip.creation.appError", + "translation": "Tidak dapat memuat kiriman" + }, + { + "id": "ent.compliance.csv.warning.appError", + "translation": "Tidak dapat memuat kiriman" + }, + { + "id": "ent.compliance.csv.metadata.json.zipfile.appError", + "translation": "Tidak dapat memuat kiriman" + }, + { + "id": "ent.account_migration.get_saml_users_failed", + "translation": "Tidak dapat memuat kiriman" + }, + { + "id": "ent.account_migration.get_all_failed", + "translation": "Tidak dapat memuat kiriman" + }, + { + "id": "bleveengine.index_user.error", + "translation": "Tidak dapat memuat kiriman" + }, + { + "id": "bleveengine.index_post.error", + "translation": "Tidak dapat memuat kiriman" + }, + { + "id": "bleveengine.index_channel.error", + "translation": "Tidak dapat memperoleh kanal" + }, + { + "id": "bleveengine.delete_user.error", + "translation": "Tidak dapat memuat kiriman" + }, + { + "id": "bleveengine.delete_post.error", + "translation": "Tidak dapat memuat kiriman" + }, + { + "id": "bleveengine.delete_channel.error", + "translation": "Tidak dapat memperoleh kanal" + }, + { + "id": "app.submit_interactive_dialog.json_error", + "translation": "Percakapan interaktif pada encoding JSON mengalami masalah ." + }, + { + "id": "app.plugin.write_file.saving.app_error", + "translation": "Terjadi kesalahan saat meninggalkan kanal" + }, + { + "id": "app.plugin.write_file.read.app_error", + "translation": "Terjadi kesalahan saat meninggalkan kanal" + }, + { + "id": "app.plugin.remove.app_error", + "translation": "Tidak dapat memuat kiriman" + }, + { + "id": "app.plugin.get_plugins.app_error", + "translation": "Tidak dapat memuat kiriman" + }, + { + "id": "app.plugin.delete_public_key.delete.app_error", + "translation": "Terjadi kesalahan saat meninggalkan kanal" + }, + { + "id": "app.plugin.deactivate.app_error", + "translation": "Tidak dapat memuat kiriman" + }, + { + "id": "app.notification.subject.notification.full", + "translation": "{{.SubjectText}} pada {{.TeamDisplayName}} pada {{.Month}} {{.Day}}, {{.Year}}" + }, + { + "id": "app.notification.subject.group_message.generic", + "translation": "[{{.SiteName}}] Notifikasi baru untuk {{.Month}} {{.Day}}, {{.Year}}[{{.SiteName}}] Notifikasi baru untuk {{.Month}} {{.Day}}, {{.Year}}" + }, + { + "id": "app.notification.subject.group_message.full", + "translation": "{{.SubjectText}} pada {{.TeamDisplayName}} pada {{.Month}} {{.Day}}, {{.Year}}" + }, + { + "id": "api.user.login.use_auth_service.app_error", + "translation": "Mohon masuk menggunakan {{.AuthService}}" + }, + { + "id": "api.user.login.not_verified.app_error", + "translation": "Masuk gagal karena alamat email belum diverifikasi" + }, + { + "id": "api.user.login.inactive.app_error", + "translation": "Gagal masuk karena akun Anda dinonaktifkan. Silakan hubungi administrator." + }, + { + "id": "api.user.login.blank_pwd.app_error", + "translation": "Kolom kata sandi harus diisi" + }, + { + "id": "api.user.ldap_to_email.not_ldap_account.app_error", + "translation": "Akun pengguna ini tidak menggunakan LDAP" + }, + { + "id": "api.user.ldap_to_email.not_available.app_error", + "translation": "LDAP tidak tersedia di server ini" + }, + { + "id": "api.user.email_to_ldap.not_available.app_error", + "translation": "LDAP tidak tersedia di server ini" + }, + { + "id": "api.user.create_user.signup_link_invalid.app_error", + "translation": "Tautan pendaftaran sepertinya tidak valid." + }, + { + "id": "api.user.create_user.signup_link_expired.app_error", + "translation": "Tautan pendaftaran telah kedaluwarsa" + }, + { + "id": "api.user.create_user.signup_email_disabled.app_error", + "translation": "Pendaftaran pengguna dengan email dinonaktifkan." + }, + { + "id": "api.user.create_user.no_open_server", + "translation": "Server ini tidak mengizinkan pendaftaran terbuka. Silakan hubungi administrator Anda untuk memperoleh undangan." + }, + { + "id": "api.user.create_user.accepted_domain.app_error", + "translation": "Email yang Anda berikan bukan bagian dari domain yang diperbolehkan. Silakan hubungi administrator Anda atau daftar dengan email yang berbeda." + }, + { + "id": "api.user.create_profile_image.initial.app_error", + "translation": "Tidak dapat menambah inisial pengguna ke gambar profil awal" + }, + { + "id": "api.user.create_profile_image.encode.app_error", + "translation": "Tidak dapat membaca kode gambar profil awal" + }, + { + "id": "api.user.create_profile_image.default_font.app_error", + "translation": "Tidak dapat membuat font gambar profil awal" + }, + { + "id": "api.user.create_oauth_user.already_attached.app_error", + "translation": "Sebuah akun dengan email tersebut yang menggunakan metode selain {{.Service}} sudah ada. Silakan masuk menggunakan {{.Auth}}." + }, + { + "id": "api.user.complete_switch_with_oauth.blank_email.app_error", + "translation": "Email kosong" + }, + { + "id": "api.user.check_user_password.invalid.app_error", + "translation": "Masuk gagal karena kata sandi tidak valid" + }, + { + "id": "api.user.check_user_mfa.bad_code.app_error", + "translation": "Token MFA tidak valid." + }, + { + "id": "api.user.check_user_login_attempts.too_many.app_error", + "translation": "Akun Anda dikunci karena terlalu banyak percobaan masuk yang gagal. Mohon setel ulang kata sandi Anda." + }, + { + "id": "api.user.authorize_oauth_user.unsupported.app_error", + "translation": "Penyedia layanan OAuth tidak didukung" + }, + { + "id": "api.user.authorize_oauth_user.token_failed.app_error", + "translation": "Permintaan token gagal" + }, + { + "id": "api.user.authorize_oauth_user.service.app_error", + "translation": "Permintaan token untuk {{.Service}} gagal" + }, + { + "id": "api.user.authorize_oauth_user.missing.app_error", + "translation": "Token akses tidak ditemukan" + }, + { + "id": "api.user.authorize_oauth_user.invalid_state.app_error", + "translation": "Keadaan tidak valid" + }, + { + "id": "api.user.authorize_oauth_user.bad_token.app_error", + "translation": "Kesalahan jenis token" + }, + { + "id": "api.user.authorize_oauth_user.bad_response.app_error", + "translation": "Kesalahan balasan dari permintaan token" + }, + { + "id": "api.user.activate_mfa.email_and_ldap_only.app_error", + "translation": "MFA tidak tersedia untuk jenis akun ini" + }, + { + "id": "api.templates.welcome_body.title", + "translation": "Anda telah bergabung dengan {{ .ServerURL }}" + }, + { + "id": "api.templates.welcome_body.info", + "translation": "Mohon verifikasi email Anda dengan menekan tombol berikut." + }, + { + "id": "api.templates.welcome_body.button", + "translation": "Verifikasi Email" + }, + { + "id": "api.templates.verify_subject", + "translation": "Verifikasi Email [{{ .SiteName }}]" + }, + { + "id": "api.templates.verify_body.title", + "translation": "Anda telah bergabung dengan {{ .ServerURL }}" + }, + { + "id": "api.templates.verify_body.info", + "translation": "Mohon verifikasi email Anda dengan menekan tombol berikut." + }, + { + "id": "api.templates.verify_body.button", + "translation": "Verifikasi Email" + }, + { + "id": "api.templates.username_change_body.title", + "translation": "Anda telah memperbarui nama pengguna" + }, + { + "id": "api.templates.signin_change_email.body.title", + "translation": "Anda telah memperbarui metode Masuk Anda" + }, + { + "id": "api.templates.signin_change_email.body.method_email", + "translation": "email dan kata sandi" + }, + { + "id": "api.templates.signin_change_email.body.info", + "translation": "Anda telah memperbarui metodi Masuk Anda pada {{ .SiteName }}" + }, + { + "id": "api.templates.reset_body.title", + "translation": "Anda telah meminta setel ulang kata sandi" + }, + { + "id": "api.templates.reset_body.button", + "translation": "Setel Ulang Sandi" + }, + { + "id": "api.templates.post_body.button", + "translation": "Ke Kiriman" + }, + { + "id": "api.templates.password_change_body.title", + "translation": "Kata sandi Anda telah diganti" + }, + { + "id": "api.templates.invite_body.title", + "translation": "Anda telah diundang" + }, + { + "id": "api.templates.invite_body.button", + "translation": "Gabung ke Tim" + }, + { + "id": "api.templates.email_organization", + "translation": "Dikirim oleh " + }, + { + "id": "api.templates.email_footer", + "translation": "Untuk mengubah pilihan notifikasi Anda, silakan masuk ke situs tim Anda, lalu buka Pengaturan Akun > Notifikasi." + }, + { + "id": "api.templates.email_change_verify_body.title", + "translation": "Anda telah memperbarui email Anda" + }, + { + "id": "api.templates.email_change_verify_body.info", + "translation": "Untuk menyelesaikan perubahan alamat email Anda untuk {{.TeamDisplayName}}, silakan klik tautan di bawah ini untuk mengonfirmasikan ini alamat yang benar." + }, + { + "id": "api.templates.email_change_verify_body.button", + "translation": "Verifikasi Email" + }, + { + "id": "api.templates.email_change_body.title", + "translation": "Anda telah memperbarui email Anda" + }, + { + "id": "api.team.update_team_scheme.scheme_scope.error", + "translation": "Tidak dapat mengatur skema ke saluran karena skema yang disediakan bukan skema saluran." + }, + { + "id": "api.team.update_team_scheme.license.error", + "translation": "Lisensi anda tidak mendukung update skema saluran" + }, + { + "id": "api.team.update_team_member_roles.guest_and_user.app_error", + "translation": "Update anggota kanal tidak valid: Pengguna harus menjadi tamu atau pengguna tetapi tidak keduanya." + }, + { + "id": "api.team.update_member_roles.not_a_member", + "translation": "Pengguna yang ditentukan bukan anggota saluran yang ditentukan." + }, + { + "id": "api.team.set_team_icon.too_large.app_error", + "translation": "Tidak dapat menunggah berkas gambar. Ukuran terlalu besar." + }, + { + "id": "api.team.set_team_icon.storage.app_error", + "translation": "Tidak dapat mengunggah berkas. Penyimpanan berkas belum diatur." + }, + { + "id": "api.team.set_team_icon.parse.app_error", + "translation": "Tidak dapat menangani bentuk multipart" + }, + { + "id": "api.team.set_team_icon.open.app_error", + "translation": "Tidak dapat membuka file gambar" + }, + { + "id": "api.team.set_team_icon.no_file.app_error", + "translation": "Tidak ada berkas 'gambar' yang diminta" + }, + { + "id": "api.team.set_team_icon.array.app_error", + "translation": "Tidak ada berkas 'gambar' di dalam permintaan" + }, + { + "id": "api.team.remove_user_from_team.removed", + "translation": "%v telah dihapus dari kanal." + }, + { + "id": "api.team.remove_user_from_team.missing.app_error", + "translation": "Pengguna sepertinya bukan bagian dari tim ini." + }, + { + "id": "api.team.remove_member.group_constrained.app_error", + "translation": "Gagal menghapus pengguna dari saluran yang dibatasi grup." + }, + { + "id": "api.team.move_channel.post.error", + "translation": "Gagal dalam mengirim pesan masuk/keluar" + }, + { + "id": "api.team.is_team_creation_allowed.disabled.app_error", + "translation": "Pembuatan Tim telah dinonaktifkan. Silakan hubungi administrator sistem Anda untuk info lebih lanjut." + }, + { + "id": "api.team.invite_members.no_one.app_error", + "translation": "Tidak ada yang dapat diundang." + }, + { + "id": "api.team.import_team.parse.app_error", + "translation": "Tidak dapat menangani bentuk multipart" + }, + { + "id": "api.team.import_team.open.app_error", + "translation": "Tidak dapat membuka berkas." + }, + { + "id": "api.team.import_team.no_file.app_error", + "translation": "Tidak ada berkas 'gambar' yang diminta" + }, + { + "id": "api.team.import_team.array.app_error", + "translation": "Tidak ada berkas 'gambar' yang diminta" + }, + { + "id": "api.team.get_team_icon.filesettings_no_driver.app_error", + "translation": "Nama driver untuk pengaturan berkas tidak valid. Harus berupa 'local' atau 'amazonS3'" + }, + { + "id": "model.user.is_valid.pwd_lowercase_uppercase.app_error", + "translation": "Kata sandi Anda harus berjumlah paling sedikit {{.Min}} karakter, dan harus terdapat minimal satu huruf kecil." + }, + { + "id": "model.user.is_valid.pwd_lowercase_symbol.app_error", + "translation": "Kata sandi Anda harus berjumlah paling sedikit {{.Min}} karakter, dan harus terdapat minimal satu huruf kecil." + }, + { + "id": "model.user.is_valid.pwd_lowercase_number_symbol.app_error", + "translation": "Kata sandi Anda harus berjumlah paling sedikit {{.Min}} karakter, dan harus terdapat minimal satu huruf kecil." + }, + { + "id": "model.user.is_valid.pwd_lowercase_number.app_error", + "translation": "Kata sandi Anda harus berjumlah paling sedikit {{.Min}} karakter, dan harus terdapat minimal satu huruf kecil." + }, + { + "id": "model.user.is_valid.pwd_lowercase.app_error", + "translation": "Kata sandi Anda harus berjumlah paling sedikit {{.Min}} karakter, dan harus terdapat minimal satu huruf kecil." + }, + { + "id": "model.user.is_valid.pwd.app_error", + "translation": "Kata sandi Anda harus berjumlah paling sedikit {{.Min}} karakter." + }, + { + "id": "model.user.is_valid.nickname.app_error", + "translation": "Nama tidak valid" + }, + { + "id": "model.user.is_valid.locale.app_error", + "translation": "Peran tidak valid" + }, + { + "id": "model.user.is_valid.last_name.app_error", + "translation": "Nama tim tidak valid" + }, + { + "id": "model.user.is_valid.id.app_error", + "translation": "ID pengguna tidak valid" + }, + { + "id": "model.user.is_valid.first_name.app_error", + "translation": "Nama depan tidak valid" + }, + { + "id": "model.user.is_valid.email.app_error", + "translation": "Email tidak valid" + }, + { + "id": "model.user.is_valid.create_at.app_error", + "translation": "Pembuatan harus saat waktu yang valid" + }, + { + "id": "model.user.is_valid.auth_data_type.app_error", + "translation": "Pengguna tidak valid, data autentikasi harus diatur dengan tipe autentikasi" + }, + { + "id": "model.user.is_valid.auth_data_pwd.app_error", + "translation": "Pengguna tidak valid, data autentikasi dan kata sandi tidak dapat diatur bersamaan" + }, + { + "id": "model.user.is_valid.auth_data.app_error", + "translation": "Data autentikasi tidak valid" + }, + { + "id": "model.token.is_valid.size", + "translation": "Token tidak valid" + }, + { + "id": "model.team_member.is_valid.user_id.app_error", + "translation": "ID pengguna tidak valid" + }, + { + "id": "model.team_member.is_valid.team_id.app_error", + "translation": "ID tim tidak valid" + }, + { + "id": "model.team.is_valid.url.app_error", + "translation": "Identifier URL tidak valid" + }, + { + "id": "model.team.is_valid.update_at.app_error", + "translation": "Pembaruan harus pada waktu yang valid" + }, + { + "id": "model.team.is_valid.type.app_error", + "translation": "Tipe tidak valid" + }, + { + "id": "model.team.is_valid.reserved.app_error", + "translation": "Tautan URL ini tidak tersedia. Mohon coba yang lainnya." + }, + { + "id": "model.team.is_valid.name.app_error", + "translation": "Nama tidak valid" + }, + { + "id": "model.team.is_valid.id.app_error", + "translation": "ID tidak valid" + }, + { + "id": "model.team.is_valid.email.app_error", + "translation": "Email tidak valid" + }, + { + "id": "model.team.is_valid.domains.app_error", + "translation": "Domain yang diperbolehkan tidak valid" + }, + { + "id": "model.team.is_valid.description.app_error", + "translation": "Deskripsi tidak valid" + }, + { + "id": "model.team.is_valid.create_at.app_error", + "translation": "Pembuatan harus saat waktu yang valid" + }, + { + "id": "model.team.is_valid.company.app_error", + "translation": "Nama perusahaan tidak valid" + }, + { + "id": "model.team.is_valid.characters.app_error", + "translation": "Nama harus berupa 2 atau lebih huruf kecil alfanumerik" + }, + { + "id": "model.reaction.is_valid.user_id.app_error", + "translation": "ID pengguna tidak valid" + }, + { + "id": "model.reaction.is_valid.post_id.app_error", + "translation": "ID root tidak valid" + }, + { + "id": "model.reaction.is_valid.emoji_name.app_error", + "translation": "ID emoji tidak valid" + }, + { + "id": "model.reaction.is_valid.create_at.app_error", + "translation": "Pembuatan harus saat waktu yang valid" + }, + { + "id": "model.preference.is_valid.value.app_error", + "translation": "Nilai terlalu panjang" + }, + { + "id": "model.preference.is_valid.theme.app_error", + "translation": "Tema tidak valid" + }, + { + "id": "model.preference.is_valid.name.app_error", + "translation": "Nama tidak valid" + }, + { + "id": "model.preference.is_valid.id.app_error", + "translation": "ID pengguna tidak valid" + }, + { + "id": "model.preference.is_valid.category.app_error", + "translation": "Kategori tidak valid" + }, + { + "id": "model.post.is_valid.user_id.app_error", + "translation": "ID pengguna tidak valid" + }, + { + "id": "model.post.is_valid.update_at.app_error", + "translation": "Pembaruan harus pada waktu yang valid" + }, + { + "id": "model.post.is_valid.type.app_error", + "translation": "Tipe tidak valid" + }, + { + "id": "model.post.is_valid.root_id.app_error", + "translation": "ID root tidak valid" + }, + { + "id": "model.post.is_valid.props.app_error", + "translation": "Prop tidak valid" + }, + { + "id": "model.post.is_valid.original_id.app_error", + "translation": "ID asli tidak valid" + }, + { + "id": "model.post.is_valid.msg.app_error", + "translation": "Pesan tidak valid" + }, + { + "id": "model.post.is_valid.id.app_error", + "translation": "ID tidak valid" + }, + { + "id": "model.post.is_valid.hashtags.app_error", + "translation": "Tagar tidak valid" + }, + { + "id": "model.post.is_valid.filenames.app_error", + "translation": "Nama berkas tidak valid" + }, + { + "id": "api.team.get_invite_info.not_open_team", + "translation": "Undangan tidak valid karena ini bukan tim terbuka." + }, + { + "id": "api.team.get_all_teams.insufficient_permissions", + "translation": "Kamu tidak memiliki hak akses yang sesuai untuk mengubah tajuk kanal" + }, + { + "id": "api.team.add_user_to_team.added", + "translation": "%v ditambahkan ke kanal oleh %v" + }, + { + "id": "api.team.add_members.error", + "translation": "Eror menambahkan anggota kanal." + }, + { + "id": "api.slackimport.slack_import.team_fail", + "translation": "Gagal mendapatkan tim untuk diimpor.\r\n" + }, + { + "id": "api.slackimport.slack_import.notes", + "translation": "\r\n Catatan \r\n" + }, + { + "id": "api.slackimport.slack_import.note2", + "translation": "- Kiriman bot Slack saat ini tidak didukung.\r\n" + }, + { + "id": "api.slackimport.slack_import.note1", + "translation": "- Beberapa kiriman mungkin tidak dapat diimpor karena tidak didukung oleh pengimpor ini.\r\n" + }, + { + "id": "api.slackimport.slack_add_users.unable_import", + "translation": "Tidak dapat mengimpor pengguna: {{.Username}}\r\n" + }, + { + "id": "api.slackimport.slack_add_users.created", + "translation": "\r\n Pengguna Dibuat\r\n" + }, + { + "id": "api.slackimport.slack_add_channels.added", + "translation": "\r\n Kanal Ditambahkan \r\n" + }, + { + "id": "api.server.start_server.starting.critical", + "translation": "Gagal memulai server, err:%v" + }, + { + "id": "api.scheme.patch_scheme.license.error", + "translation": "Lisensi anda tidak mendukung update skema saluran" + }, + { + "id": "api.scheme.get_teams_for_scheme.scope.error", + "translation": "Tidak dapat mengatur skema ke saluran karena skema yang disediakan bukan skema saluran." + }, + { + "id": "api.scheme.get_channels_for_scheme.scope.error", + "translation": "Tidak dapat mengatur skema ke saluran karena skema yang disediakan bukan skema saluran." + }, + { + "id": "api.scheme.delete_scheme.license.error", + "translation": "Lisensi anda tidak mendukung update skema saluran" + }, + { + "id": "api.scheme.create_scheme.license.error", + "translation": "Lisensi anda tidak mendukung update skema saluran" + }, + { + "id": "api.roles.patch_roles.license.error", + "translation": "Lisensi anda tidak mendukung update skema saluran" + }, + { + "id": "api.preference.update_preferences.set.app_error", + "translation": "Tidak dapat memuat kiriman" + }, + { + "id": "api.preference.preferences_category.get.app_error", + "translation": "Tidak dapat memuat kiriman" + }, + { + "id": "api.preference.delete_preferences.delete.app_error", + "translation": "Tidak dapat memuat kiriman" + }, + { + "id": "api.post_get_post_by_id.get.app_error", + "translation": "Tidak dapat memuat kiriman" + }, + { + "id": "api.post.update_post.system_message.app_error", + "translation": "Tidak dapat memperbarui pesan sistem" + }, + { + "id": "api.post.update_post.permissions_details.app_error", + "translation": "Telah dihapus id={{.PostId}}" + }, + { + "id": "api.post.update_post.find.app_error", + "translation": "Kami tidak dapat menemukan kiriman yang telah ada atau komentar untuk diperbarui" + }, + { + "id": "api.post.send_notifications_and_forget.push_explicit_mention", + "translation": " memanggil Anda di " + }, + { + "id": "api.post.link_preview_disabled.app_error", + "translation": "Tautan publik dinonaktfikan oleh administrator sistem." + }, + { + "id": "api.post.do_action.action_id.app_error", + "translation": "ID klien tidak valid" + }, + { + "id": "api.post.create_webhook_post.creating.app_error", + "translation": "Gagal membuat kiriman" + }, + { + "id": "api.post.create_post.root_id.app_error", + "translation": "Parameter RootId tidak valid" + }, + { + "id": "api.outgoing_webhook.disabled.app_error", + "translation": "Webhooks keluar telah dinonaktfikan oleh admin sistem." + }, + { + "id": "api.oauth.singup_with_oauth.invalid_link.app_error", + "translation": "Tautan pendaftaran sepertinya tidak valid." + }, + { + "id": "api.oauth.singup_with_oauth.expired_link.app_error", + "translation": "Tautan pendaftaran telah kedaluwarsa" + }, + { + "id": "api.oauth.revoke_access_token.get.app_error", + "translation": "Terjadi kesalahan saat mengakses token dari DB sebelum penghapusan" + }, + { + "id": "api.oauth.revoke_access_token.del_token.app_error", + "translation": "Terjadi kesalahan menghapus token akses dari DB" + }, + { + "id": "api.oauth.revoke_access_token.del_session.app_error", + "translation": "Terjadi kesalahan menghapus sesi dari DB" + }, + { + "id": "api.oauth.register_oauth_app.turn_off.app_error", + "translation": "Admin sistem telah mematikan layanan OAuth." + }, + { + "id": "api.oauth.invalid_state_token.app_error", + "translation": "Token MFA tidak valid." + }, + { + "id": "api.oauth.get_access_token.internal_user.app_error", + "translation": "server_error: Terjadi kesalahan server internal saat mengambil data pengguna dari basis data" + }, + { + "id": "api.oauth.get_access_token.internal_session.app_error", + "translation": "server_error: Terjadi kesalahan server internal saat menyimpan sesi ke basisdata" + }, + { + "id": "api.oauth.get_access_token.internal_saving.app_error", + "translation": "server_error: Terjadi kesalahan server internal saat menyimpan sesi ke basisdata" + }, + { + "id": "api.oauth.get_access_token.internal.app_error", + "translation": "server_error: Terjadi kesalahan server internal saat menyimpan sesi ke basisdata" + }, + { + "id": "api.oauth.get_access_token.disabled.app_error", + "translation": "Admin sistem telah mematikan layanan OAuth." + }, + { + "id": "api.oauth.get_access_token.bad_grant.app_error", + "translation": "invalid_request: Kesalahan grant_type" + }, + { + "id": "api.oauth.get_access_token.bad_client_id.app_error", + "translation": "invalid_request: Kesalahan grant_type" + }, + { + "id": "api.oauth.authorize_oauth.disabled.app_error", + "translation": "Admin sistem telah mematikan layanan OAuth." + }, + { + "id": "api.oauth.allow_oauth.turn_off.app_error", + "translation": "Admin sistem telah mematikan layanan OAuth." + }, + { + "id": "api.license.client.old_format.app_error", + "translation": "Format baru untuk konfigurasi klien belum didukung. Harap tentukan ormat=old dalam string kueri." + }, + { + "id": "api.license.add_license.unique_users.app_error", + "translation": "Lisensi ini hanya mendukung sebanyak {{.Users}} pengguna, namun sistem Anda memiliki {{.Count}} pengguna unik. Pengguna unik dihitung dari jumlah email yang berbeda. Anda dapat melihat total jumlah pengguna di Laporan Situs -> Lihat Statistik." + }, + { + "id": "api.license.add_license.save_active.app_error", + "translation": "ID lisensi aktif tidak disimpan secara sempurna." + }, + { + "id": "api.license.add_license.save.app_error", + "translation": "Lisensi tidak disimpan secara sempurna." + }, + { + "id": "api.license.add_license.open.app_error", + "translation": "Tidak dapat membuka berkas lisensi" + }, + { + "id": "api.license.add_license.no_file.app_error", + "translation": "Tidak ada berkas 'lisensi' yang diminta" + }, + { + "id": "api.license.add_license.invalid_count.app_error", + "translation": "Tidak dapat menghitung jumlah pengguna unik." + }, + { + "id": "api.license.add_license.invalid.app_error", + "translation": "Berkas lisensi tidak valid." + }, + { + "id": "api.license.add_license.expired.app_error", + "translation": "Lisensi telah kadaluwarsa atau belum dimulai." + }, + { + "id": "api.license.add_license.array.app_error", + "translation": "Tidak ada berkas 'gambar' yang diminta" + }, + { + "id": "api.incoming_webhook.invalid_username.app_error", + "translation": "Nama tidak valid" + }, + { + "id": "api.incoming_webhook.disabled.app_error", + "translation": "Webhooks masuk telah dinonaktifkan oleh admin sistem." + }, + { + "id": "api.file.upload_file.storage.app_error", + "translation": "Tidak dapat mengunggah berkas. Penyimpanan berkas belum diatur." + }, + { + "id": "api.file.read_file.reading_local.app_error", + "translation": "Terjadi kesalahan saat membaca dari penyimpanan server lokal" + }, + { + "id": "api.file.get_public_link.no_post.app_error", + "translation": "Tidak dapat memperoleh tautan publik untuk data. Data harus dilampirkan pada postingan yang bisa dibaca oleh pengguna saat ini." + }, + { + "id": "api.file.get_public_link.disabled.app_error", + "translation": "Tautan publik telah dinonaktifkan." + }, + { + "id": "api.file.get_file_thumbnail.no_thumbnail.app_error", + "translation": "Data tidak memiliki gambar ukuran kecil" + }, + { + "id": "api.file.get_file_preview.no_preview.app_error", + "translation": "Data tidak memiliki pratinjau gambar" + }, + { + "id": "api.file.get_file.public_invalid.app_error", + "translation": "Tautan publik tampaknya tidak valid." + }, + { + "id": "api.file.attachments.disabled.app_error", + "translation": "Data yang dilampirkan tidak diizinkan oleh server ini." + }, + { + "id": "api.emoji.upload.open.app_error", + "translation": "Tidak dapat membuat emoji. Terjadi kesalahan saat proses pengkodean gambar." + }, + { + "id": "api.emoji.upload.large_image.gif_encode_error", + "translation": "Tidak dapat membuat emoji. Terjadi kesalahan saat proses pengkodean gambar GIF." + }, + { + "id": "api.emoji.upload.large_image.gif_decode_error", + "translation": "Tidak dapat membuat emoji. Terjadi kesalahan saat proses membaca kode gambar GIF." + }, + { + "id": "api.emoji.upload.large_image.encode_error", + "translation": "Tidak dapat membuat emoji. Terjadi kesalahan saat proses pengkodean gambar." + }, + { + "id": "api.emoji.upload.large_image.decode_error", + "translation": "Tidak dapat membuat emoji. Terjadi kesalahan saat proses membaca kode gambar." + }, + { + "id": "api.emoji.upload.image.app_error", + "translation": "Tidak dapat membuat emoji. Berkas harus berupa PNG, JPEG, atau GIF." + }, + { + "id": "api.emoji.storage.app_error", + "translation": "Penyimpanan berkas belum diatur dengan tepat. Silakan atur penyimpanan berkas dengan menggunakan S3 atau server lokal." + }, + { + "id": "api.emoji.get_image.read.app_error", + "translation": "Tidak dapat membaca berkas gambar untuk emoji." + }, + { + "id": "api.emoji.get_image.decode.app_error", + "translation": "Tidak dapat membaca kode berkas gambar untuk emoji." + }, + { + "id": "api.emoji.disabled.app_error", + "translation": "Emoji khusus telah dinonaktifkan oleh admin sistem." + }, + { + "id": "api.emoji.create.too_large.app_error", + "translation": "Tidak dapat membuat emoji. Ukuran gambar harus kurang dari 1 MB." + }, + { + "id": "api.emoji.create.parse.app_error", + "translation": "Tidak dapat membuat emoji. Tidak dapat memahami permintaan." + }, + { + "id": "api.emoji.create.other_user.app_error", + "translation": "ID pengguna tidak valid" + }, + { + "id": "api.emoji.create.duplicate.app_error", + "translation": "Tidak dapat membuat emoji. Sudah ada emoji lain dengan nama yang sama." + }, + { + "id": "api.email_batching.send_batched_email_notification.subject", + "translation": { + "other": "[{{.SiteName}}] Notifikasi baru untuk {{.Month}} {{.Day}}, {{.Year}}[{{.SiteName}}] Notifikasi baru untuk {{.Month}} {{.Day}}, {{.Year}}" + } + }, + { + "id": "api.email_batching.add_notification_email_to_batch.disabled.app_error", + "translation": "Pengarsipan email telah dimatikan oleh administrator sistem" + }, + { + "id": "api.email_batching.add_notification_email_to_batch.channel_full.app_error", + "translation": "Kanal pengiriman \"email batching job\" sudah penuh. Tolong tingkatkan EmailBatchingBufferSize." + }, + { + "id": "api.context.token_provided.app_error", + "translation": "Sesi bukan OAuth tapi token diberikan dalam string kueri" + }, + { + "id": "api.context.session_expired.app_error", + "translation": "Sesi tidak valid atau kadaluwarsa. Mohon masuk kembali." + }, + { + "id": "api.context.permissions.app_error", + "translation": "Anda tidak memiliki hak akses" + }, + { + "id": "api.context.mfa_required.app_error", + "translation": "Autentikasi multi faktor diperlukan pada server ini." + }, + { + "id": "api.context.invalid_url_param.app_error", + "translation": "Parameter tidak valid atau hilang {{.Name}} di permintaan URL" + }, + { + "id": "api.context.invalid_token.error", + "translation": "Token sesi tidak valid = {{. Token}}, err = {{. Error}}" + }, + { + "id": "api.context.invalid_param.app_error", + "translation": "Parameter {{.Name}} tidak valid" + }, + { + "id": "api.context.invalid_body_param.app_error", + "translation": "Tidak valid atau hilang {{.Name}} di badan permintaan" + }, + { + "id": "api.context.404.app_error", + "translation": "Maaf, tidak dapat menemukan halaman yang dimaksud" + }, + { + "id": "api.config.client.old_format.app_error", + "translation": "Format baru untuk konfigurasi klien belum didukung. Harap tentukan ormat=old dalam string kueri." + }, + { + "id": "api.command_shrug.name", + "translation": "shrug" + }, + { + "id": "api.command_shrug.hint", + "translation": "[pesan]" + }, + { + "id": "api.command_shrug.desc", + "translation": "Tambahkan ¯\\_(ツ)_/¯ ke pesan Anda" + }, + { + "id": "api.command_shortcuts.unsupported.app_error", + "translation": "Perintah pintas tidak didukung di perangkat anda" + }, + { + "id": "api.command_shortcuts.name", + "translation": "pintas" + }, + { + "id": "api.command_shortcuts.desc", + "translation": "Tampilkan daftar pintasan keyboard" + }, + { + "id": "api.command_settings.unsupported.app_error", + "translation": "Perintah pengaturan tidak di dukung di perangkat anda" + }, + { + "id": "api.command_settings.name", + "translation": "pengaturan" + }, + { + "id": "api.command_settings.desc", + "translation": "Buka dialog Pengaturan Akun" + }, + { + "id": "api.command_search.unsupported.app_error", + "translation": "Perintah cari tidak didukung di perangkat anda" + }, + { + "id": "api.command_search.name", + "translation": "cari" + }, + { + "id": "api.command_search.hint", + "translation": "[text]" + }, + { + "id": "api.command_search.desc", + "translation": "Cari teks di pesan" + }, + { + "id": "api.command_remove.permission.app_error", + "translation": "Kamu tidak memiliki hak akses yang sesuai untuk mengubah nama kanal" + }, + { + "id": "api.command_remove.message.app_error", + "translation": "Pesan harus disediakan dengan perintah /rename ." + }, + { + "id": "api.command_open.name", + "translation": "buka" + }, + { + "id": "api.command_online.success", + "translation": "Kamu sekarang terhubung" + }, + { + "id": "api.command_online.name", + "translation": "terhubung" + }, + { + "id": "api.command_online.desc", + "translation": "Atur status anda menjadi terhubung" + }, + { + "id": "api.command_offline.success", + "translation": "Kamu sekarang terputus" + }, + { + "id": "api.command_offline.name", + "translation": "terputus" + }, + { + "id": "api.command_offline.desc", + "translation": "Tetapkan status Anda offline" + }, + { + "id": "api.command_mute.success_mute_direct_msg", + "translation": "Anda tidak akan menerima notifikasi untuk {{.Channel}} sampai saluran mute dinonaktifkan." + }, + { + "id": "api.command_mute.success_mute", + "translation": "Anda tidak akan menerima notifikasi untuk {{.Channel}} sampai saluran mute dinonaktifkan." + }, + { + "id": "api.command_msg.permission.app_error", + "translation": "Kamu tidak memiliki hak akses yang sesuai untuk mengubah nama kanal" + }, + { + "id": "api.command_msg.name", + "translation": "pesan" + }, + { + "id": "api.command_msg.missing.app_error", + "translation": "Tidak dapat memperoleh kanal" + }, + { + "id": "api.command_msg.hint", + "translation": "@[username] 'pesan'" + }, + { + "id": "api.command_msg.fail.app_error", + "translation": "Terjadi kesalahan saat mengirimkan pesan ke pengguna." + }, + { + "id": "api.command_msg.dm_fail.app_error", + "translation": "Galat saat membuat pesan langsung." + }, + { + "id": "api.command_msg.desc", + "translation": "Kirim Pesan Langsung ke pengguna" + }, + { + "id": "api.command_me.name", + "translation": "saya" + }, + { + "id": "api.command_me.hint", + "translation": "[pesan]" + }, + { + "id": "api.command_me.desc", + "translation": "Lakukan aksi" + }, + { + "id": "api.command_logout.name", + "translation": "keluar" + }, + { + "id": "api.command_logout.desc", + "translation": "Keluar dari Mattermost" + }, + { + "id": "api.command_leave.name", + "translation": "tinggalkan" + }, + { + "id": "api.command_leave.fail.app_error", + "translation": "Terjadi kesalahan saat meninggalkan kanal" + }, + { + "id": "api.command_leave.desc", + "translation": "Tinggalkan kanal sekarang" + }, + { + "id": "api.command_join.name", + "translation": "gabung" + }, + { + "id": "api.command_join.missing.app_error", + "translation": "Tidak dapat memperoleh kanal" + }, + { + "id": "api.command_join.list.app_error", + "translation": "Kesalahan terjadi ketika membuka file." + }, + { + "id": "api.command_join.fail.app_error", + "translation": "Kesalahan terjadi ketika bergabung ke kanal." + }, + { + "id": "api.command_join.desc", + "translation": "Gabung kanal terbuka" + }, + { + "id": "api.command_invite.fail.app_error", + "translation": "Kesalahan terjadi ketika bergabung ke kanal." + }, + { + "id": "api.command_invite.channel.app_error", + "translation": "Galat mengambil kanal termutakhir" + }, + { + "id": "api.command_help.name", + "translation": "bantuan" + }, + { + "id": "api.command_help.desc", + "translation": "Buka halaman bantuan Mattermost" + }, + { + "id": "api.command_groupmsg.name", + "translation": "pesan" + }, + { + "id": "api.command_groupmsg.group_fail.app_error", + "translation": "Galat saat membuat pesan langsung." + }, + { + "id": "api.command_groupmsg.fail.app_error", + "translation": "Terjadi kesalahan saat mengirimkan pesan ke pengguna." + }, + { + "id": "api.command_expand_collapse.fail.app_error", + "translation": "Terjadi galat sewaktu mengekstrak berkas." + }, + { + "id": "api.command_expand.success", + "translation": "Alamat gambar sekarang diluaskan secara umum" + }, + { + "id": "api.command_expand.name", + "translation": "Kembangkan" + }, + { + "id": "api.command_expand.desc", + "translation": "Matikan fitur tutup-otomatis dari peninjauan gambar" + }, + { + "id": "api.command_echo.name", + "translation": "Gema" + }, + { + "id": "api.command_echo.message.app_error", + "translation": "Pesan harus disediakan dengan perintah /echo." + }, + { + "id": "api.command_echo.hint", + "translation": "'message' [waktu tunda dalam detik]" + }, + { + "id": "api.command_echo.high_volume.app_error", + "translation": "Volume tinggi untuk permintaan gema, tidak dapat menangani permintaan" + }, + { + "id": "api.command_echo.desc", + "translation": "Gema teks balik dari akun anda" + }, + { + "id": "api.command_echo.delay.app_error", + "translation": "Waktu tunda harus dibawah 10000 detik" + }, + { + "id": "api.command_collapse.success", + "translation": "Alamat gambar sekarang diciutkan secara umum" + }, + { + "id": "api.command_collapse.name", + "translation": "ciutkan" + }, + { + "id": "api.command_collapse.desc", + "translation": "Matikan fitur tutup-otomatis dari gambar pratinjau" + }, + { + "id": "api.command_code.name", + "translation": "kode" + }, + { + "id": "api.command_code.message.app_error", + "translation": "Pesan harus disediakan dengan perintah /echo ." + }, + { + "id": "api.command_code.hint", + "translation": "[teks]" + }, + { + "id": "api.command_code.desc", + "translation": "Tampilkan teks sebagai blok kode" + }, + { + "id": "api.command_channel_rename.update_channel.app_error", + "translation": "Galat dalam memperbaharui kanal sekarang." + }, + { + "id": "api.command_channel_rename.too_short.app_error", + "translation": "Nama kanal harus {{.Length}} atau lebih huruf" + }, + { + "id": "api.command_channel_rename.too_long.app_error", + "translation": "Nama kanal harus {{.Length}} atau lebih huruf" + }, + { + "id": "api.command_channel_rename.permission.app_error", + "translation": "Kamu tidak memiliki hak akses yang sesuai untuk mengubah nama kanal" + }, + { + "id": "api.command_channel_rename.name", + "translation": "ubah nama" + }, + { + "id": "api.command_channel_rename.message.app_error", + "translation": "Pesan harus disediakan dengan perintah /rename ." + }, + { + "id": "api.command_channel_rename.hint", + "translation": "[teks]" + }, + { + "id": "api.command_channel_rename.direct_group.app_error", + "translation": "Tidak dapat keluar dari kanal pesan langsung." + }, + { + "id": "api.command_channel_rename.desc", + "translation": "Ubah nama kanal" + }, + { + "id": "api.command_channel_rename.channel.app_error", + "translation": "Galat mengambil kanal termutakhir." + }, + { + "id": "api.command_channel_remove.channel.app_error", + "translation": "Galat mengambil kanal termutakhir" + }, + { + "id": "api.command_channel_purpose.update_channel.app_error", + "translation": "Galat dalam memperbaharui kanal sekarang." + }, + { + "id": "api.command_channel_purpose.permission.app_error", + "translation": "Kamu tidak memiliki hak akses yang sesuai untuk mengubah tujuan kanal" + }, + { + "id": "api.command_channel_purpose.name", + "translation": "tujuan" + }, + { + "id": "api.command_channel_purpose.message.app_error", + "translation": "Pesan harus disediakan dengan perintah /purpose ." + }, + { + "id": "api.command_channel_purpose.hint", + "translation": "[teks]" + }, + { + "id": "api.command_channel_purpose.desc", + "translation": "Ubah tujuan kanal" + }, + { + "id": "api.command_channel_purpose.channel.app_error", + "translation": "Galat mengambil kanal termutakhir." + }, + { + "id": "api.command_channel_header.update_channel.app_error", + "translation": "Galat dalam memperbaharui kanal sekarang" + }, + { + "id": "api.command_channel_header.permission.app_error", + "translation": "Kamu tidak memiliki hak akses yang sesuai untuk mengubah tajuk kanal" + }, + { + "id": "api.command_channel_header.name", + "translation": "Tajuk" + }, + { + "id": "api.command_channel_header.message.app_error", + "translation": "Teks harus disediakan dengan perintah /header" + }, + { + "id": "api.command_channel_header.hint", + "translation": "[teks]" + }, + { + "id": "api.command_channel_header.desc", + "translation": "Ubah header kanal" + }, + { + "id": "api.command_channel_header.channel.app_error", + "translation": "Galat mengambil kanal termutakhir" + }, + { + "id": "api.command_away.success", + "translation": "Kamu sekarang tidak ditempat" + }, + { + "id": "api.command_away.name", + "translation": "tidak ditempat" + }, + { + "id": "api.command_away.desc", + "translation": "Atur status anda ke 'tidak ditempat'" + }, + { + "id": "api.command.team_mismatch.app_error", + "translation": "Tidak dapat memperbaharui perintah melewati tim" + }, + { + "id": "api.command.invite_people.sent", + "translation": "Email undangan telah dikirim" + }, + { + "id": "api.command.invite_people.no_email", + "translation": "Mohon masukkan satu atau lebih alamat email yang valid" + }, + { + "id": "api.command.invite_people.name", + "translation": "undang_orang" + }, + { + "id": "api.command.invite_people.invite_off", + "translation": "Pembuatan pengguna baru telah dinonaktifkan pada server ini, tidak ada undangan yang dikirim" + }, + { + "id": "api.command.invite_people.hint", + "translation": "[nama@domain.com ...]" + }, + { + "id": "api.command.invite_people.fail", + "translation": "Kesalahan dalam mengirimkan email undangan" + }, + { + "id": "api.command.invite_people.email_off", + "translation": "Email belum diatur, tidak ada undangan yang dikirimkan" + }, + { + "id": "api.command.invite_people.desc", + "translation": "Kirimkan email undangan ke tim Mattermost anda" + }, + { + "id": "api.command.execute_command.start.app_error", + "translation": "Tidak dapat menemukan perintah pemicu" + }, + { + "id": "app.notification.subject.direct.full", + "translation": "{{.SubjectText}} pada {{.TeamDisplayName}} pada {{.Month}} {{.Day}}, {{.Year}}" + }, + { + "id": "app.import.validate_scheme_import_data.name_invalid.error", + "translation": "Nama tidak valid" + }, + { + "id": "app.import.validate_scheme_import_data.display_name_invalid.error", + "translation": "Tampilan nama tidak valid" + }, + { + "id": "app.import.validate_scheme_import_data.description_invalid.error", + "translation": "Deskripsi tidak valid" + }, + { + "id": "app.import.validate_role_import_data.name_invalid.error", + "translation": "Nama tidak valid" + }, + { + "id": "app.import.validate_role_import_data.display_name_invalid.error", + "translation": "Tampilan nama tidak valid" + }, + { + "id": "app.import.validate_role_import_data.description_invalid.error", + "translation": "Deskripsi tidak valid" + }, + { + "id": "app.import.import_direct_channel.update_header_failed.error", + "translation": "Tidak dapat memperoleh kanal" + }, + { + "id": "app.channel.post_update_channel_purpose_message.updated_to", + "translation": "%s memperbarui header kanal menjadi: %s" + }, + { + "id": "app.channel.post_update_channel_purpose_message.updated_from", + "translation": "%s memperbarui eheader kanal dari: %s ke: %s" + }, + { + "id": "app.channel.post_update_channel_purpose_message.retrieve_user.error", + "translation": "Gagal mendapatkan pengguna saat mencoba menyimpan perubahan pesan header kanal %v" + }, + { + "id": "app.channel.post_update_channel_purpose_message.removed", + "translation": "%s menghapus header kanal (sebelumnya: %s)" + }, + { + "id": "app.channel.post_update_channel_purpose_message.post.error", + "translation": "Gagal dalam mengirim pesan masuk/keluar" + }, + { + "id": "app.channel.create_direct_channel.internal_error", + "translation": "Tidak dapat memperoleh kanal" + }, + { + "id": "app.channel.create_channel.internal_error", + "translation": "Tidak dapat memperoleh kanal" + }, + { + "id": "app.bot.permenent_delete.bad_id", + "translation": "Tidak dapat memuat kiriman" + }, + { + "id": "app.bot.patchbot.internal_error", + "translation": "Tidak dapat memuat kiriman" + }, + { + "id": "app.bot.getbots.internal_error", + "translation": "Tidak dapat memuat kiriman" + }, + { + "id": "app.bot.getbot.internal_error", + "translation": "Tidak dapat memuat kiriman" + }, + { + "id": "app.bot.createbot.internal_error", + "translation": "Tidak dapat memuat kiriman" + }, + { + "id": "api.websocket_handler.invalid_param.app_error", + "translation": "Parameter {{.Name}} tidak valid" + }, + { + "id": "api.webhook.update_outgoing.intersect.app_error", + "translation": "Webhooks keluar dari kanal yang sama tidak boleh memiliki kata pemicu (trigger)/callback URL yang sama." + }, + { + "id": "api.webhook.team_mismatch.app_error", + "translation": "Tidak dapat memperbaharui perintah melewati tim" + }, + { + "id": "api.webhook.create_outgoing.triggers.app_error", + "translation": "Antara trigger_words atau channel_id harus diatur" + }, + { + "id": "api.webhook.create_outgoing.permissions.app_error", + "translation": "Tidak memiliki hak akses yang sesuai untuk membuat webhook keluar." + }, + { + "id": "api.webhook.create_outgoing.not_open.app_error", + "translation": "Webhooks keluar hanya dapat dibuat untuk kanal publik." + }, + { + "id": "api.webhook.create_outgoing.intersect.app_error", + "translation": "Webhooks keluar dari kanal yang sama tidak boleh memiliki kata pemicu (trigger)/callback URL yang sama." + }, + { + "id": "api.web_socket.connect.upgrade.app_error", + "translation": "Gagal meningkatkan koneksi websocket" + }, + { + "id": "api.user.verify_email.bad_link.app_error", + "translation": "Tautan verifikasi email rusak." + }, + { + "id": "api.user.upload_profile_user.upload_profile.app_error", + "translation": "Tidak dapat mengunggah gambar profil" + }, + { + "id": "api.user.upload_profile_user.too_large.app_error", + "translation": "Tidak dapat mengunggah gambar profil. Berkas terlalu besar." + }, + { + "id": "api.user.upload_profile_user.storage.app_error", + "translation": "Tidak dapat mengunggah berkas. Penyimpanan gambar belum diatur." + }, + { + "id": "api.user.upload_profile_user.parse.app_error", + "translation": "Tidak dapat menangani bentuk multipart" + }, + { + "id": "api.user.upload_profile_user.open.app_error", + "translation": "Tidak dapat membuka file gambar" + }, + { + "id": "api.user.upload_profile_user.no_file.app_error", + "translation": "Tidak ada berkas 'gambar' yang diminta" + }, + { + "id": "api.user.upload_profile_user.encode.app_error", + "translation": "Tidak dapat membaca kode gambar profil" + }, + { + "id": "api.user.upload_profile_user.decode.app_error", + "translation": "Tidak dapat membaca kode gambar profil" + }, + { + "id": "api.user.upload_profile_user.array.app_error", + "translation": "Tidak ada berkas 'gambar' di dalam permintaan" + }, + { + "id": "api.user.update_user.accepted_guest_domain.app_error", + "translation": "Email yang Anda berikan bukan bagian dari domain yang diperbolehkan. Silakan hubungi administrator Anda atau daftar dengan email yang berbeda." + }, + { + "id": "api.user.update_user.accepted_domain.app_error", + "translation": "Email yang Anda berikan bukan bagian dari domain yang diperbolehkan. Silakan hubungi administrator Anda atau daftar dengan email yang berbeda." + }, + { + "id": "api.user.update_password.valid_account.app_error", + "translation": "Perubahan kata sandi gagal karena kami tidak dapat menemukan akun yang valid" + }, + { + "id": "api.user.update_password.oauth.app_error", + "translation": "Perubahan kata sandi gagal karena pengguna telah masuk melalui layanan OAuth" + }, + { + "id": "api.user.update_password.menu", + "translation": "menggunakan menu pengaturan" + }, + { + "id": "api.user.update_password.incorrect.app_error", + "translation": "\"Kata Sandi Saat Ini\" yang Anda masukkan salah. Mohon periksa kembali apakah Caps Lock tidak aktif dan coba kembali." + }, + { + "id": "api.user.update_password.failed.app_error", + "translation": "Perubahan kata sandi gagal" + }, + { + "id": "api.user.update_password.context.app_error", + "translation": "Perubahan kata sandi gagal karena konteks user_id tidak cocok dengan user_id yang ditentukan" + }, + { + "id": "api.user.update_active.permissions.app_error", + "translation": "Anda tidak memiliki hak akses" + }, + { + "id": "api.user.send_verify_email_and_forget.failed.error", + "translation": "Gagal mengirimkan email verifikasi" + }, + { + "id": "api.user.send_sign_in_change_email_and_forget.error", + "translation": "Gagal mengirimkan notifikasi email untuk perubahan kata sandi" + }, + { + "id": "api.user.send_password_reset.sso.app_error", + "translation": "Tidak dapat melakukan penyetelan-ulang kata sandi untuk akun SSO" + }, + { + "id": "api.user.send_password_reset.send.app_error", + "translation": "Gagal mengirim email penyetelan-ulang kata sandi" + }, + { + "id": "api.user.send_email_change_verify_email_and_forget.error", + "translation": "Gagal mengirimkan notifikasi perubahan email" + }, + { + "id": "api.user.saml.not_available.app_error", + "translation": "SAML tidak diatur atau didukung oleh server ini" + }, + { + "id": "api.user.reset_password.sso.app_error", + "translation": "Tidak dapat melakukan penyetelan-ulang kata sandi untuk akun SSO" + }, + { + "id": "api.user.reset_password.method", + "translation": "menggunakan tautan penyetelan-ulang kata sandi" + }, + { + "id": "api.user.reset_password.link_expired.app_error", + "translation": "Tautan penyetelan-ulang kata sandi telah kedaluwarsa" + }, + { + "id": "api.user.reset_password.invalid_link.app_error", + "translation": "Tautan penyetelan-ulang kata sandi tidak valid" + }, + { + "id": "api.user.reset_password.broken_token.app_error", + "translation": "Tautan penyetelan-ulang kata sandi tidak valid" + }, + { + "id": "api.user.oauth_to_email.context.app_error", + "translation": "Pembaruan kata sandi gagal karena konteks user_id tidak sesuai dengan ID pengguna yang diberikan" + }, + { + "id": "api.user.login_ldap.not_available.app_error", + "translation": "LDAP tidak tersedia di server ini" + }, + { + "id": "api.command.execute_command.not_found.app_error", + "translation": "Perintah dengan pemicu dari '{{.Trigger}}' tidak ditemukan. Untuk mengirim pesan dimulai dengan \"/\", dan coba tambahkan spasi di awal pesan." + }, + { + "id": "api.command.execute_command.failed_resp.app_error", + "translation": "Perintah dengan pemicu '{{.Trigger}}' menghasilkan respon {{.Status}}" + }, + { + "id": "api.command.execute_command.failed_empty.app_error", + "translation": "Perintah dengan pemicu '{{.Trigger}}' menghasilkan respon kosong" + }, + { + "id": "api.command.execute_command.failed.app_error", + "translation": "Perintah dengan pemicu '{{.Trigger}}' gagal" + }, + { + "id": "api.command.execute_command.create_post_failed.app_error", + "translation": "Perintah '{{.Trigger}}' gagal mengirim tanggapan. Silakan hubungi Administrator Sistem Anda." + }, + { + "id": "api.command.duplicate_trigger.app_error", + "translation": "Kata pemicu ini telah digunakan. Silakan pilih kata lainnya." + }, + { + "id": "api.command.disabled.app_error", + "translation": "Perintah telah dimatikan oleh admin sistem." + }, + { + "id": "api.command.command_post.forbidden.app_error", + "translation": "Pengguna yang ditentukan bukan anggota saluran yang ditentukan." + }, + { + "id": "api.command.admin_only.app_error", + "translation": "Integrasi telah dibatasi untuk admin saja." + }, + { + "id": "api.channel.update_team_member_roles.scheme_role.app_error", + "translation": "Peran yang disediakan dikelola oleh Skema dan karena itu tidak dapat diterapkan langsung ke Anggota Tim" + }, + { + "id": "api.channel.update_team_member_roles.changing_guest_role.app_error", + "translation": "Pembaruan anggota tim tidak valid: Anda tidak dapat menambah atau menghapus peran tamu secara manual" + }, + { + "id": "api.channel.update_channel_scheme.scheme_scope.error", + "translation": "Tidak dapat mengatur skema ke saluran karena skema yang disediakan bukan skema saluran." + }, + { + "id": "api.channel.update_channel_scheme.license.error", + "translation": "Lisensi anda tidak mendukung update skema saluran" + }, + { + "id": "api.channel.update_channel_privacy.default_channel_error", + "translation": "Saluran default tidak dapat dibuat pribadi." + }, + { + "id": "api.channel.update_channel_member_roles.scheme_role.app_error", + "translation": "Peran yang disediakan diatur oleh Skema dan tidak bisa dimenerapkan langsung ke Pengguna Kanal" + }, + { + "id": "api.channel.update_channel_member_roles.guest_and_user.app_error", + "translation": "Update anggota kanal tidak valid: Pengguna harus menjadi tamu atau pengguna tetapi tidak keduanya." + }, + { + "id": "api.channel.update_channel_member_roles.changing_guest_role.app_error", + "translation": "Update anggota kanal tidak valid: Anda tidak bisa mentambahkan atau menghapus peran tamu secara manual." + }, + { + "id": "api.channel.update_channel.typechange.app_error", + "translation": "Tipe kanal tidak bisa di update" + }, + { + "id": "api.channel.update_channel.tried.app_error", + "translation": "Mencoba melakukan pembaharuan tidak valid ke kanal umum {{.Channel}}" + }, + { + "id": "api.channel.update_channel.deleted.app_error", + "translation": "Kanal telah diarsipkan atau dihapus" + }, + { + "id": "api.channel.restore_channel.unarchived", + "translation": "{{.Username}} telah unarchive kanal ini." + }, + { + "id": "api.channel.restore_channel.restored.app_error", + "translation": "Gagal untuk unarchive kanal. Kanal ini tidak bisa di unarchive." + }, + { + "id": "api.channel.rename_channel.cant_rename_group_messages.app_error", + "translation": "Anda tidak bisa mengganti nama saluran pesan grup (group message channel)" + }, + { + "id": "api.channel.rename_channel.cant_rename_direct_messages.app_error", + "translation": "Anda tidak dapat mengganti nama saluran pesan langsung (direct message channel)" + }, + { + "id": "api.channel.remove_user_from_channel.app_error", + "translation": "Gagal menghapus pengguna dari jenis kanal ini" + }, + { + "id": "api.channel.remove_members.denied", + "translation": "Penghapusan keanggotaan saluran ditolak oleh pengguna (pengguna-pengguna) berikut karena kendala grup: {{.UserIDs}}" + }, + { + "id": "api.channel.remove_member.removed", + "translation": "%v telah dihapus dari kanal." + }, + { + "id": "api.channel.remove_member.group_constrained.app_error", + "translation": "Gagal menghapus pengguna dari saluran yang dibatasi grup." + }, + { + "id": "api.channel.remove_channel_member.type.app_error", + "translation": "Gagal menghapus anggota dari kanal." + }, + { + "id": "api.channel.remove.default.app_error", + "translation": "Tidak dapat menghapus pengguna dari kanal umum {{.Channel}}" + }, + { + "id": "api.channel.post_user_add_remove_message_and_forget.error", + "translation": "Gagal dalam mengirim pesan masuk/keluar" + }, + { + "id": "api.channel.post_update_channel_header_message_and_forget.updated_to", + "translation": "%s memperbaharui header kanal menjadi: %s" + }, + { + "id": "api.channel.post_update_channel_header_message_and_forget.updated_from", + "translation": "%s memperbaharui header kanal dari: %s ke: %s" + }, + { + "id": "api.channel.post_update_channel_header_message_and_forget.retrieve_user.error", + "translation": "Gagal mendapatkan pengguna ketika mencoba merubah header kanal" + }, + { + "id": "api.channel.post_update_channel_header_message_and_forget.removed", + "translation": "%s menghapus kop kanal (sebelumnya: %s)" + }, + { + "id": "api.channel.post_update_channel_header_message_and_forget.post.error", + "translation": "Galat ketika mengirim kop pesan kanal" + }, + { + "id": "api.channel.post_update_channel_displayname_message_and_forget.updated_from", + "translation": "%s memperbaharui nama display kanal dari: %s ke: %s" + }, + { + "id": "api.channel.post_update_channel_displayname_message_and_forget.retrieve_user.error", + "translation": "Gagal mendapatkan pengguna ketika mencoba menyimpan perubahan kanal DisplayName" + }, + { + "id": "api.channel.post_update_channel_displayname_message_and_forget.create_post.error", + "translation": "Gagal memposting pesan mutakhir displayname" + }, + { + "id": "api.channel.post_channel_privacy_message.error", + "translation": "Gagal untuk mengirim pesanan kanal update pribadi." + }, + { + "id": "api.channel.patch_update_channel.forbidden.app_error", + "translation": "Gagal meng-update kanal" + }, + { + "id": "api.channel.patch_channel_moderations.license.error", + "translation": "Lisensi Anda tidak mendukung autentikasi SAML." + }, + { + "id": "api.channel.leave.left", + "translation": "%v telah keluar dari kanal." + }, + { + "id": "api.channel.leave.last_member.app_error", + "translation": "Anda satu-satunya anggota yang tersisa, coba menghapus Kanal Pribadi daripada keluar." + }, + { + "id": "api.channel.leave.direct.app_error", + "translation": "Tidak dapat keluar dari kanal pesan langsung" + }, + { + "id": "api.channel.leave.default.app_error", + "translation": "Tidak dapat keluar dari kanal default {{.Channel}}" + }, + { + "id": "api.channel.join_channel.post_and_forget", + "translation": "%v telah bergabung ke kanal." + }, + { + "id": "api.channel.join_channel.permissions.app_error", + "translation": "Anda tidak memiliki hak akses" + }, + { + "id": "api.channel.guest_join_channel.post_and_forget", + "translation": "% v bergabung dengan saluran sebagai tamu." + }, + { + "id": "api.channel.get_channel_moderations.license.error", + "translation": "Lisensi Anda tidak mendukung autentikasi SAML." + }, + { + "id": "api.channel.delete_channel.deleted.app_error", + "translation": "Kanal telah diarsipkan atau dihapus" + }, + { + "id": "api.channel.delete_channel.cannot.app_error", + "translation": "Tidak dapat menghapus kanal default {{.Channel}}" + }, + { + "id": "api.channel.delete_channel.archived", + "translation": "%v telah mengarsipkan kanal." + }, + { + "id": "api.channel.create_group.bad_user.app_error", + "translation": "Salah satu pengguna yang disediakan tidak ada" + }, + { + "id": "api.channel.create_group.bad_size.app_error", + "translation": "Kanal pesan grup harus berisi minimal 3 dan tidak lebih dari 8 pengguna" + }, + { + "id": "api.channel.create_direct_channel.invalid_user.app_error", + "translation": "ID pengguna tidak valid untuk pembuatan kanal langsung" + }, + { + "id": "api.channel.create_default_channels.town_square", + "translation": "Taman Kota" + }, + { + "id": "api.channel.create_default_channels.off_topic", + "translation": "Topik bebas" + }, + { + "id": "api.channel.create_channel.max_channel_limit.app_error", + "translation": "Tidak dapat membuat kanal lebih dari {{.MaxChannelsPerTeam}} untuk tim ini " + }, + { + "id": "api.channel.create_channel.direct_channel.app_error", + "translation": "Harus menggunakan layanan api createDirectChannel untuk pembuatan kanal pesan langsung" + }, + { + "id": "api.channel.change_channel_privacy.public_to_private", + "translation": "Kanal ini telah diubah menjadi Kanal Pribadi." + }, + { + "id": "api.channel.change_channel_privacy.private_to_public", + "translation": "Kanal ini telah diubah menjadi Kanal Publik dan dapat diikuti oleh anggota tim manapun." + }, + { + "id": "api.channel.add_user_to_channel.type.app_error", + "translation": "Tidak dapat menambahkan pengguna ke jenis kanal ini" + }, + { + "id": "api.channel.add_user.to.channel.failed.deleted.app_error", + "translation": "Gagal menambahkan pengguna ke kanal karena dia telah dihapus dari tim." + }, + { + "id": "api.channel.add_user.to.channel.failed.app_error", + "translation": "Gagal menambahkan pengguna ke kanal" + }, + { + "id": "api.channel.add_members.user_denied", + "translation": "Keanggotaan kanal ditolak untuk pengguna-pengguna berikut karena kendala kolompok : {{ .UserIDs }}" + }, + { + "id": "api.channel.add_members.error", + "translation": "Eror menambahkan anggota kanal." + }, + { + "id": "api.channel.add_member.added", + "translation": "%v ditambahkan ke kanal oleh %v" + }, + { + "id": "api.channel.add_guest.added", + "translation": "%v ditambahkan ke kanal oleh %v sebagai tamu." + }, + { + "id": "api.bot.teams_channels.add_message_mobile", + "translation": "Tolong tambahkan (add) saya ke tim dan channel yang anda ingin saya interaksi di. Untuk mengatifkan fitur ini, tolong pakai browser atau Mattermost Desktop App." + }, + { + "id": "api.admin.upload_brand_image.too_large.app_error", + "translation": "Gagal mengunggah berkas. Berkas terlalu besar." + }, + { + "id": "api.admin.upload_brand_image.storage.app_error", + "translation": "Gagal mengunggap gambar. Penyimpanan gambar belum diatur." + }, + { + "id": "api.admin.upload_brand_image.parse.app_error", + "translation": "Tidak dapat menangani bentuk multipart" + }, + { + "id": "api.admin.upload_brand_image.no_file.app_error", + "translation": "Tidak ada berkas 'gambar' yang diminta" + }, + { + "id": "api.admin.upload_brand_image.array.app_error", + "translation": "Tidak ada berkas 'gambar' di dalam permintaan" + }, + { + "id": "api.admin.test_email.subject", + "translation": "Mattermost - Pengaturan Tes Email" + }, + { + "id": "api.admin.test_email.reenter_password", + "translation": "Server SMTP, port, atau nama pengguna telah berubah. Silahkan inputkan ulang kata sandi SMTP server untuk mengecek koneksi. " + }, + { + "id": "api.admin.test_email.missing_server", + "translation": "SMTP Server dibutuhkan" + }, + { + "id": "api.admin.test_email.body", + "translation": "Sepertinya pengaturan email Mattermost Anda telah benar!" + }, + { + "id": "api.admin.saml.set_certificate_from_metadata.invalid_body.app_error", + "translation": "Teks sertifikat tidak valid." + }, + { + "id": "api.admin.saml.not_available.app_error", + "translation": "SAML 2.0 tidak dikonfigurasikan atau didukung oleh server ini." + }, + { + "id": "api.admin.saml.metadata.app_error", + "translation": "Terjadi kesalahan ketika membuat Metadata Penyedia Layanan." + }, + { + "id": "api.admin.saml.failure_save_idp_certificate_file.app_error", + "translation": "Tidak dapat menyimpan berkas sertifikat." + }, + { + "id": "api.admin.saml.failure_get_metadata_from_idp.app_error", + "translation": "Gagal mendapatkan metadata dari URL Penyedia Identitas." + }, + { + "id": "api.admin.remove_certificate.delete.app_error", + "translation": "Terjadi kesalahan saat menghapus sertifikat." + }, + { + "id": "api.admin.get_brand_image.storage.app_error", + "translation": "Penyimpanan gambar belum diatur." + }, + { + "id": "api.admin.file_read_error", + "translation": "Gagal membaca berkas log." + }, + { + "id": "api.admin.add_certificate.saving.app_error", + "translation": "Tidak dapat menyimpan berkas sertifikat." + }, + { + "id": "api.admin.add_certificate.open.app_error", + "translation": "Tidak dapat membuka berkas sertifikat." + }, + { + "id": "api.admin.add_certificate.no_file.app_error", + "translation": "Tidak ada berkas 'sertifikat' di dalam permintaan." + }, + { + "id": "api.admin.add_certificate.array.app_error", + "translation": "Berkas yang diminta tidak ada didalam 'certificate'." + }, + { + "id": "September", + "translation": "September" + }, + { + "id": "October", + "translation": "Oktober" + }, + { + "id": "November", + "translation": "November" + }, + { + "id": "May", + "translation": "Mei" + }, + { + "id": "March", + "translation": "Maret" + }, + { + "id": "June", + "translation": "Juni" + }, + { + "id": "July", + "translation": "Juli" + }, + { + "id": "January", + "translation": "Januari" + }, + { + "id": "February", + "translation": "Februari" + }, + { + "id": "December", + "translation": "Desember" + }, + { + "id": "August", + "translation": "Agustus" + }, + { + "id": "April", + "translation": "April" + }, + { + "id": "model.group.name.invalid_length.app_error", + "translation": "Nama harus berupa 1 hingga 64 huruf kecil karakter alfanumerik " + }, + { + "id": "api.admin.delete_brand_image.storage.not_found", + "translation": "Tidak dapat menghapus gambar merek, tidak ditemukan." + }, + { + "id": "api.admin.ldap.not_available.app_error", + "translation": "LDAP tidak tersedia." + }, + { + "id": "api.admin.add_certificate.parseform.app_error", + "translation": "Terjadi kesalahan saat mengurai permintaan beraneka ragam" + } +] diff --git a/server/i18n/is.json b/server/i18n/is.json new file mode 100644 index 00000000000..0967ef424bc --- /dev/null +++ b/server/i18n/is.json @@ -0,0 +1 @@ +{} diff --git a/server/i18n/it.json b/server/i18n/it.json index 24c1fd89a47..5cd62f842ab 100644 --- a/server/i18n/it.json +++ b/server/i18n/it.json @@ -4043,10 +4043,6 @@ "id": "model.emoji.user_id.app_error", "translation": "Id creatore non valido." }, - { - "id": "model.file_info.get.gif.app_error", - "translation": "Impossibile decodificare la gif." - }, { "id": "model.file_info.is_valid.create_at.app_error", "translation": "Valore non valido per il parametro create_at." @@ -7307,14 +7303,6 @@ "id": "app.sharedchannel.dm_channel_creation.internal_error", "translation": " " }, - { - "id": "app.post.get_top_threads_for_user_since.app_error", - "translation": " " - }, - { - "id": "app.post.get_top_threads_for_team_since.app_error", - "translation": " " - }, { "id": "app.import.generate_password.app_error", "translation": " " @@ -7615,10 +7603,6 @@ "id": "api.templates.cloud_welcome_email.button", "translation": " " }, - { - "id": "api.insights.feature_disabled", - "translation": " " - }, { "id": "api.file.write_file.app_error", "translation": " " @@ -8139,10 +8123,6 @@ "id": "api.templates.invite_team_and_channel_subject", "translation": " " }, - { - "id": "app.insights.feature_disabled", - "translation": " " - }, { "id": "api.templates.questions_footer.title", "translation": " " @@ -8615,10 +8595,6 @@ "id": "app.notification.body.thread_gm.subTitle", "translation": " " }, - { - "id": "app.recent_searches.app_error", - "translation": " " - }, { "id": "app.user.get_by_auth.other.app_error", "translation": " " diff --git a/server/i18n/ja.json b/server/i18n/ja.json index 31ca619103a..12a0ccf7214 100644 --- a/server/i18n/ja.json +++ b/server/i18n/ja.json @@ -4035,10 +4035,6 @@ "id": "model.emoji.user_id.app_error", "translation": "不正なcreator idです。" }, - { - "id": "model.file_info.get.gif.app_error", - "translation": "GIFをデコードできません。" - }, { "id": "model.file_info.is_valid.create_at.app_error", "translation": "create_atが不正な値です。" @@ -4749,7 +4745,7 @@ }, { "id": "web.error.unsupported_browser.min_browser_version.edge", - "translation": "Version 110+" + "translation": "Version 112+" }, { "id": "web.error.unsupported_browser.min_browser_version.chrome", @@ -9087,10 +9083,6 @@ "id": "api.license.request_trial_license.embargoed", "translation": "禁輸国に対する制限のため、リクエストを処理することができませんでした。輸出制限に関するご質問は、[説明文書を参照するか](https://mattermost.com/pl/limitations-for-embargoed-countries)、legal@mattermost.com までお問い合わせください。" }, - { - "id": "api.insights.feature_disabled", - "translation": "インサイトは有効化されていない機能フラグに含まれる機能です。" - }, { "id": "api.getThreadsForUser.bad_only_params", "translation": "getThreadsForUserに対するOnlyThreadsとOnlyTotalsのパラメータが矛盾しています" @@ -9115,10 +9107,6 @@ "id": "api.elasticsearch.test_elasticsearch_settings_nil.app_error", "translation": "Elasticsearchの設定に未設定の値があります。" }, - { - "id": "app.recent_searches.app_error", - "translation": "最近の検索履歴を取得する際にエラーが発生しました" - }, { "id": "api.file.cloud_upload.app_error", "translation": "クラウドインスタンスへのmmctlによるアップロードはサポートされていません。こちらのドキュメントを確認してください:https://docs.mattermost.com/manage/cloud-data-export.html。" @@ -9147,18 +9135,6 @@ "id": "api.cloud.teams_limit_reached.create", "translation": "チーム数の制限に達したため、チームを作成できませんでした" }, - { - "id": "app.post.get_top_threads_for_user_since.app_error", - "translation": "ユーザーのトップスレッドを取得できませんでした。" - }, - { - "id": "app.post.get_top_threads_for_team_since.app_error", - "translation": "チームのトップスレッドを取得できませんでした。" - }, - { - "id": "app.insights.feature_disabled", - "translation": "インサイト機能は無効化されています。" - }, { "id": "api.cloud.notify_admin_to_upgrade_error.already_notified", "translation": "管理者に通知済み" @@ -9191,10 +9167,6 @@ "id": "app.post_reminder_dm", "translation": "@{{.Username}}からのこのメッセージについてのリマインダーです: {{.SiteURL}}/{{.TeamName}}/pl/{{.PostId}}" }, - { - "id": "app.post.get_top_dms_for_user_since.app_error", - "translation": "ユーザーのトップDMを取得できませんでした。" - }, { "id": "app.cloud.get_subscription_delinquency_date.app_error", "translation": "サブスクリプションは滞納されていません" @@ -9459,14 +9431,6 @@ "id": "api.team.invite_guests_to_channels.disabled.error", "translation": "ゲストアカウントは無効化されています" }, - { - "id": "app.collection.add_topic.exists.app_error", - "translation": "Topic type はすでに存在しています。" - }, - { - "id": "app.collection.add_collection.exists.app_error", - "translation": "Collection typeはすでに存在しています。" - }, { "id": "model.draft.is_valid.user_id.app_error", "translation": "不正なuser idです。" @@ -9591,22 +9555,10 @@ "id": "api.acknowledgement.delete.archived_channel.app_error", "translation": "アーカイブされたチャンネルでは、確認応答を削除することはできません。" }, - { - "id": "worktemplate.category.product_teams", - "translation": "プロダクト" - }, { "id": "model.draft.is_valid.priority.app_error", "translation": "不正な優先度" }, - { - "id": "app.worktemplates.get_templates.app_error", - "translation": "作業テンプレートを取得できませんでした" - }, - { - "id": "app.worktemplates.get_categories.app_error", - "translation": "作業テンプレートのカテゴリを取得できませんでした" - }, { "id": "api.user.get_users.validation.app_error", "translation": "検証中のロール取得時にエラーが発生しました。" @@ -9667,74 +9619,10 @@ "id": "api.license.true_up_review.user_count_fail", "translation": "総アクティブユーザー数を取得できませんでした" }, - { - "id": "worktemplate.category.leadership", - "translation": "リーダーシップ" - }, - { - "id": "worktemplate.category.devops", - "translation": "DevOps" - }, - { - "id": "worktemplate.category.companywide", - "translation": "会社全体" - }, - { - "id": "app.worktemplates.execute_work_template.playbooks.find_channel_error", - "translation": "Playbookに関連するチャンネルが見つかりませんでした。" - }, - { - "id": "app.worktemplates.execute_work_template.playbooks.create_error", - "translation": "Playbookを作成する際にエラーが発生しました。" - }, - { - "id": "app.worktemplates.execute_work_template.channels.create_error", - "translation": "チャンネルを作成する際にエラーが発生しました。" - }, - { - "id": "app.worktemplates.execute_work_template.boards.create_error", - "translation": "Boardを作成する際にエラーが発生しました。" - }, - { - "id": "app.worktemplates.execute_work_template.app_error", - "translation": "作業テンプレートを実行する際にエラーが発生しました。" - }, - { - "id": "app.worktemplate.execution_request.license_cannot_create_private_playbook", - "translation": "あなたのライセンスは非公開Playbooksをサポートしていません。" - }, - { - "id": "app.worktemplate.execution_request.cannot_create_public_playbook", - "translation": "公開Playbookを作成する権限がありません。" - }, - { - "id": "app.worktemplate.execution_request.cannot_create_public_channel", - "translation": "公開チャンネルを作成する権限がありません。" - }, - { - "id": "app.worktemplate.execution_request.cannot_create_public_board", - "translation": "公開Boardを作成する権限がありません。" - }, - { - "id": "app.worktemplate.execution_request.cannot_create_private_playbook", - "translation": "非公開Playbookを作成する権限がありません。" - }, - { - "id": "app.worktemplate.execution_request.cannot_create_private_channel", - "translation": "非公開チャンネルを作成する権限がありません。" - }, - { - "id": "app.worktemplate.execution_request.cannot_create_private_board", - "translation": "非公開Boardを作成する権限がありません。" - }, { "id": "app.group.username_conflict", "translation": "ユーザー名 \"{{.Username}}\" のユーザーは既に存在します。" }, - { - "id": "api.work_templates.disabled", - "translation": "作業テンプレートは無効です。" - }, { "id": "api.license.true_up_review.not_allowed_for_cloud", "translation": "棚卸しはクラウドインスタンスには許可されていません" @@ -9755,46 +9643,10 @@ "id": "api.config.update_config.translations.app_error", "translation": "サーバーの翻訳を更新できませんでした。" }, - { - "id": "api.command_templates.name", - "translation": "templates" - }, { "id": "api.server.cws.delete_workspace.app_error", "translation": "CWS サーバーはワークスペースの削除に失敗しました。" }, - { - "id": "api.command_templates.desc", - "translation": "テンプレートウィンドウから作成を開く" - }, - { - "id": "app.worktemplates.execute_work_template.name_too_long", - "translation": "名称欄には64文字以上入力できません。" - }, - { - "id": "app.team.create_onboarding_linked_board.product_key_not_found", - "translation": "プロダクトキーが見つかりませんでした" - }, - { - "id": "app.team.create_onboarding_linked_board.itf_not_of_type", - "translation": "インターフェースが期待された型ではありませんでした" - }, - { - "id": "app.team.create_onboarding_linked_board.error_patching_board", - "translation": "Boardへのパッチ適用エラー" - }, - { - "id": "app.team.create_onboarding_linked_board.error_no_board", - "translation": "Board作成エラー" - }, - { - "id": "app.team.create_onboarding_linked_board.error_getting_templates", - "translation": "Boardsテンプレート取得エラー" - }, - { - "id": "app.team.create_onboarding_linked_board.error_duplicating_board", - "translation": "重複したBoardによるエラー" - }, { "id": "app.import.validate_user_teams_import_data.invalid_auth_service.error", "translation": "不正な認証サービス: {{.AuthService}}" @@ -9807,338 +9659,28 @@ "id": "api.server.cws.needs_enterprise_edition", "translation": "Mattermost Enterprise Editionでのみ利用可能なサービスです" }, - { - "id": "api.command_templates.unsupported.app_error", - "translation": "あなたのデバイスではテンプレートコマンドはサポートされていません。" - }, - { - "id": "app.user.run.update_status.title", - "translation": "ステータスの更新" - }, - { - "id": "app.user.run.update_status.submit_label", - "translation": "ステータスを更新" - }, - { - "id": "app.user.run.update_status.reminder_for_next_update", - "translation": "次回更新のリマインド" - }, - { - "id": "app.user.run.update_status.num_channel", - "translation": { - "other": "関係者に更新内容を提供します。この投稿は {{.Count}} チャンネルに配信されます。" - } - }, - { - "id": "app.user.run.update_status.finish_run.placeholder", - "translation": "また、実行を終了としてマークする" - }, - { - "id": "app.user.run.update_status.finish_run", - "translation": "実行を終了する" - }, - { - "id": "app.user.run.update_status.change_since_last_update", - "translation": "前回更新時からの変更点" - }, - { - "id": "app.user.run.status_enable", - "translation": "@{{.Username}} は [{{.RunName}}]({{.RunURL}}) のステータス更新を有効化しました" - }, - { - "id": "app.user.run.status_disable", - "translation": "@{{.Username}} は [{{.RunName}}]({{.RunURL}}) のステータス更新を無効化しました" - }, - { - "id": "app.user.run.request_update", - "translation": "@here — @{{.Name}} は [{{.RunName}}]({{.RunURL}}) のステータスの更新を要求しました。 \n" - }, - { - "id": "app.user.run.request_join_channel", - "translation": "@{{.Name}} は実行の参加者で、このチャンネルへの参加を希望しています。チャンネルのメンバーなら誰でも招待できます。\n" - }, - { - "id": "app.user.run.confirm_finish.title", - "translation": "実行終了の確認" - }, - { - "id": "app.user.run.confirm_finish.submit_label", - "translation": "実行を終了する" - }, - { - "id": "app.user.run.confirm_finish.num_outstanding", - "translation": { - "other": "**{{.Count}} 個の未解決タスク**があります。 本当に実行 *{{.RunName}}* を終了してもよろしいですか?" - } - }, { "id": "app.user.run.add_to_timeline.title", "translation": "実行のタイムラインに追加する" }, - { - "id": "app.user.run.add_to_timeline.summary.placeholder", - "translation": "タイムラインに表示される短い要約" - }, - { - "id": "app.user.run.add_to_timeline.summary.help", - "translation": "最大 64 文字" - }, - { - "id": "app.user.run.add_to_timeline.summary", - "translation": "概要" - }, - { - "id": "app.user.run.add_to_timeline.submit_label", - "translation": "実行のタイムラインに追加する" - }, - { - "id": "app.user.run.add_to_timeline.playbook_run", - "translation": "Playbookを実行" - }, - { - "id": "app.user.run.add_checklist_item.title", - "translation": "新しいタスクを追加" - }, - { - "id": "app.user.run.add_checklist_item.submit_label", - "translation": "タスクを追加" - }, - { - "id": "app.user.run.add_checklist_item.name", - "translation": "名前" - }, - { - "id": "app.user.run.add_checklist_item.description", - "translation": "説明" - }, - { - "id": "app.user.new_run.title", - "translation": "Playbookを実行する" - }, - { - "id": "app.user.new_run.submit_label", - "translation": "実行開始" - }, - { - "id": "app.user.new_run.run_name", - "translation": "実行名" - }, - { - "id": "app.user.new_run.playbook", - "translation": "Playbook" - }, - { - "id": "app.user.new_run.intro", - "translation": "**オーナー** {{.Username}}" - }, - { - "id": "app.user.digest.tasks.zero_assigned", - "translation": "割り当てられたタスクがありません。" - }, { "id": "app.user.digest.tasks.num_assigned_due_until_today", "translation": { "other": "対応期限を迎えている{{.Count}}タスクが割り当てられています:" } }, - { - "id": "app.user.digest.tasks.num_assigned", - "translation": { - "other": "{{.Count}}タスクが割り当てられています:" - } - }, - { - "id": "app.user.digest.tasks.heading", - "translation": "あなたに割り当てられたタスク" - }, - { - "id": "app.user.digest.tasks.due_yesterday", - "translation": "昨日で期限切れ" - }, - { - "id": "app.user.digest.tasks.due_x_days_ago", - "translation": "{{.Count}}日前に期限切れ" - }, - { - "id": "app.user.digest.tasks.due_today", - "translation": "今日が期限です" - }, - { - "id": "app.user.digest.tasks.due_in_x_days", - "translation": { - "other": "期限は{{.Count}}日後です" - } - }, - { - "id": "app.user.digest.tasks.due_after_today", - "translation": { - "other": "**今日で期限切れとなるタスクが {{.Count}} 件**割り当てられています。" - } - }, - { - "id": "app.user.digest.tasks.all_tasks_command", - "translation": "`/playbook todo`を使用すると、あなたのすべてのタスクを確認することができます。" - }, - { - "id": "app.user.digest.runs_in_progress.zero_in_progress", - "translation": "進行中の実行はありません。" - }, - { - "id": "app.user.digest.runs_in_progress.num_in_progress", - "translation": { - "other": "現在、 {{.Count}} の実行が進行中です:" - } - }, - { - "id": "app.user.digest.runs_in_progress.heading", - "translation": "進行中の実行" - }, - { - "id": "app.user.digest.overdue_status_updates.zero_overdue", - "translation": "期限切れの実行は 0 です。" - }, - { - "id": "app.user.digest.overdue_status_updates.num_overdue", - "translation": { - "other": "ステータス更新の期日が過ぎた実行が {{.Count}} あります:" - } - }, - { - "id": "app.user.digest.overdue_status_updates.heading", - "translation": "期限切れステータスの更新" - }, { "id": "app.oauth.remove_auth_data_by_client_id.app_error", "translation": "oauth データを削除することができませんでした。" }, - { - "id": "app.command.execute.error", - "translation": "コマンドを実行できませんでした。" - }, { "id": "api.templates.license_up_for_renewal_contact_sales", "translation": "営業に問い合わせる" }, - { - "id": "worktemplate.product_teams.sprint_planning.integration", - "translation": "Zoomなどのよく使われるツールと統合し、チャンネルでの生産性を高めていきましょう。これらはあなたのためにダウンロードされます。" - }, - { - "id": "worktemplate.product_teams.sprint_planning.channel", - "translation": "Boardsや統合機能と簡単に連携できるチャンネルで、チームと会話しましょう。" - }, - { - "id": "worktemplate.product_teams.sprint_planning.board", - "translation": "週次目標に向けてスプリントへの分解、優先順位付け、オーナー割り当て、コメントなどを行い、チームの進捗を追跡しましょう。" - }, - { - "id": "worktemplate.product_teams.product_roadmap.channel", - "translation": "利用者からのフィードバックや優先順位についてチームで会話し、一緒に進捗を確認しましょう。" - }, - { - "id": "worktemplate.product_teams.product_roadmap.board", - "translation": "プロダクトロードマップBoardを使用して、ユーザーフィードバックの管理、リソースの割り当て、カレンダービューでの成果物の表示、課題の優先順位付などを行いましょう。" - }, - { - "id": "worktemplate.product_teams.goals_and_okrs.integration", - "translation": "簡単にコラボレーションできるようにするためにZoomなどのよく使うツールを統合し、チャンネルの生産性を向上しましょう。これらのツールは、あなたのためにダウンロードされます。" - }, - { - "id": "worktemplate.product_teams.goals_and_okrs.channel", - "translation": "目標や進捗についてチームと会話し、一つのチャンネルで最新の状況が把握することができるようにしましょう。" - }, - { - "id": "worktemplate.product_teams.goals_and_okrs.board", - "translation": "GoalsやOKRのBoardを使って、組織目標の達成に向けたチームの進捗を確認しましょう。ミーティングアジェンダBoardを使い、ミーティングを計画的に進めましょう。" - }, - { - "id": "worktemplate.product_teams.feature_release.description.playbook", - "translation": "機能開発プロセスをサポートするタスクチェックリストと自動化機能により、部門を超えたコラボレーションを促進しましょう。開発が完了したら、レトロスペクティブを実施し、次のリリースに向けて改善しましょう。" - }, - { - "id": "worktemplate.product_teams.feature_release.description.integration", - "translation": "機能のリリースをサポートするGitHubなどのよく使うツールを統合し、チャンネルの生産性を向上しましょう。これらのツールは、あなたのためにダウンロードされます。" - }, - { - "id": "worktemplate.product_teams.feature_release.description.channel", - "translation": "Boards、Playbooks、その他の統合機能と簡単に連携できるチャンネルで、リリースに向けた問題点や変更点についてチームで会話しましょう。" - }, - { - "id": "worktemplate.product_teams.feature_release.description.board", - "translation": "ミーティングアジェンダBoardで会議を滞りなく進めましょう。また、プロジェクトタスクBoardで仕事量を管理しましょう。" - }, - { - "id": "worktemplate.product_teams.bug_bash.playbook", - "translation": "チェックリストを使ってテスト担当領域を割り当て、自動タスクを使って包括的なバグバッシュプロセスを遂行しましょう。レトロスペクティブを利用して、プロセスの見直しを行い、次回に向けて改善しましょう。" - }, - { - "id": "worktemplate.product_teams.bug_bash.integration", - "translation": "Bug bashの進捗を追跡するためにJiraなどのよく使うツールを統合し、チャンネルの生産性を向上しましょう。これらのツールは、あなたのためにダウンロードされます。" - }, - { - "id": "worktemplate.product_teams.bug_bash.channel", - "translation": "チームが簡単にアクセスできるチャンネルで、バグ報告や解決策を計画・管理しましょう。" - }, { "id": "worktemplate.leadership.goals_and_okrs.integration", "translation": "簡単にコラボレーションできるようにするためにZoomなどのよく使うツールを統合し、チャンネルの生産性を向上しましょう。これらのツールは、あなたのためにダウンロードされます。" }, - { - "id": "worktemplate.leadership.goals_and_okrs.channel", - "translation": "目標や進捗についてチームと会話し、一つのチャンネルで最新の状況が把握することができるようにしましょう。" - }, - { - "id": "worktemplate.leadership.goals_and_okrs.board", - "translation": "GoalsやOKRのBoardを使って、組織目標の達成に向けたチームの進捗を確認しましょう。ミーティングアジェンダBoardを使い、ミーティングを計画的に進めましょう。" - }, - { - "id": "worktemplate.devops.product_release.playbook", - "translation": "プロダクトのリリースを確実かつ予定通りに行うために、簡単に実行可能な再現性のあるワークフローを作成しましょう。" - }, - { - "id": "worktemplate.devops.product_release.channel", - "translation": "日々のマイルストーンや問題点、成果物に対する変更について簡潔かつ素早くチームで会話しましょう。" - }, - { - "id": "worktemplate.devops.product_release.board", - "translation": "プロダクトリリースBoardを使って、リリースに向けた期間やプロセスを把握し、全員がどのタスクに期限があるかを把握できるようにしましょう。" - }, - { - "id": "worktemplate.devops.incident_resolution.description.playbook", - "translation": "チェックリストや、主要なチームメンバーを集める自動化を利用して、インシデント解決に向けた作業状況を共有しましょう。" - }, - { - "id": "worktemplate.devops.incident_resolution.description.channel", - "translation": "一つのチャンネルの中で優先順位についてチームで会話し、関係者を追加し、最新情報を提供し、課題解決に向けて作業しましょう。" - }, - { - "id": "worktemplate.devops.incident_resolution.description.board", - "translation": "インシデント解決Boardを使用して、チーム全体で再現性のあるプロセスをサポートし、定義済みのタスクを割り当てます。" - }, - { - "id": "worktemplate.companywide.goals_and_okrs.integration", - "translation": "簡単にコラボレーションできるようにするためにZoomなどのよく使うツールを統合し、チャンネルの生産性を向上しましょう。これらのツールは、あなたのためにダウンロードされます。" - }, - { - "id": "worktemplate.companywide.goals_and_okrs.channel", - "translation": "目標や進捗についてチームと会話し、一つのチャンネルで最新の状況が把握することができるようにしましょう。" - }, - { - "id": "worktemplate.companywide.goals_and_okrs.board", - "translation": "GoalsやOKRのBoardを使って、組織目標の達成に向けたチームの進捗を確認しましょう。ミーティングアジェンダBoardを使い、ミーティングを計画的に進めましょう。" - }, - { - "id": "worktemplate.companywide.create_project.integration", - "translation": "よく使われるツールと統合し、チャンネルでの生産性を高めていきましょう。これらはあなたのためにダウンロードされます。" - }, - { - "id": "worktemplate.companywide.create_project.channel", - "translation": "共同作業を行うためのチャンネルで新しいプロジェクトについてチームと会話し、どのように進めていくか決定してきましょう。" - }, - { - "id": "worktemplate.companywide.create_project.board", - "translation": "カンバンボードを使用してタスクと進捗を定義し、追跡しましょう。" - }, { "id": "model.license_record.is_valid.bytes.app_error", "translation": "ライセンスをアップロードする際の bytes が不正な値です。" @@ -10151,10 +9693,6 @@ "id": "app.plugin.skip_installation.app_error", "translation": "プラグイン {{.Id}} は、既存のバージョンと同じかより新しいため、インストールをスキップします。" }, - { - "id": "app.plugin.blocked.app_error", - "translation": "プラグイン {{.Id}} はブロックリストに登録されています。いくつかのプラグインは、このバージョンの Mattermost に組み込まれているためブロックされています。" - }, { "id": "api.server.cws.subscribe_to_newsletter.app_error", "translation": "CWSサーバーがニュースレターを購読できませんでした。" @@ -10162,5 +9700,177 @@ { "id": "api.license.request-trial.bad-request.business-email", "translation": "トライアル用のビジネス用電子メールが不正です" + }, + { + "id": "app.post.delete_post.get_team.app_error", + "translation": "チームの取得中にエラーが発生しました。" + }, + { + "id": "app.post_persistent_notification.delete_by_channel.app_error", + "translation": "チャンネル別の永続的な通知を削除できませんでした。" + }, + { + "id": "api.license.true_up_review.failed_to_submit", + "translation": "棚卸しプロファイルをCWSに送信できませんでした。" + }, + { + "id": "api.post.post_priority.max_recipients_persistent_notification_post.request_error", + "translation": "永続的な通知は、最大 {{.MaxRecipients}} 人の受信者への通知が許可されています。" + }, + { + "id": "api.error_no_organization_name_provided_for_self_hosted_onboarding", + "translation": "エラー: セルフホストのオンボーディングで組織名が指定されていません。" + }, + { + "id": "api.post.post_priority.min_recipients_persistent_notification_post.request_error", + "translation": "削除されない通知では、少なくとも1人の受信対象の指定が必要です。" + }, + { + "id": "app.post_priority.delete_persistent_notification_post.app_error", + "translation": "永続的な通知の投稿を削除できませんでした" + }, + { + "id": "model.channel_member.is_valid.channel_auto_follow_threads_value.app_error", + "translation": "channel-auto-follow-threadsの値が不正です。" + }, + { + "id": "model.config.is_valid.persistent_notifications_count.app_error", + "translation": "投稿ごとの永続的な通知の総数が不正です。正の数である必要があります。" + }, + { + "id": "model.config.is_valid.persistent_notifications_interval.app_error", + "translation": "永続的な通知の間隔が不正です。2分以上でなくてはなりません。" + }, + { + "id": "app.post_persistent_notification.delete_by_team.app_error", + "translation": "チーム別の永続的な通知を削除できませんでした。" + }, + { + "id": "model.config.is_valid.persistent_notifications_recipients.app_error", + "translation": "永続的な通知の最大受信者数が不正です。正の数である必要があります。" + }, + { + "id": "api.command_exportlink.desc", + "translation": "エクスポートをダウンロードするためのリンクを生成します。" + }, + { + "id": "api.command_exportlink.empty.app_error", + "translation": "エクスポートファイルが見つかりません。" + }, + { + "id": "api.command_exportlink.hint", + "translation": "[job-id|zip filename|{{.LatestMsg}}]" + }, + { + "id": "api.command_exportlink.invalid.app_error", + "translation": "要求されたファイルが見つかりませんでした。" + }, + { + "id": "api.command_exportlink.link.text", + "translation": "ファイルはこちらからダウンロードできます: {{.Link}}。\nこのリンクの有効期限は {{.Expiration}} です。" + }, + { + "id": "api.command_exportlink.list.app_error", + "translation": "エクスポートファイルを取得できませんでした。" + }, + { + "id": "api.command_exportlink.name", + "translation": "exportlink" + }, + { + "id": "api.command_exportlink.permission.app_error", + "translation": "このコマンドを実行するのに十分な権限がありません。" + }, + { + "id": "api.command_exportlink.presign.app_error", + "translation": "Presign URLを生成できませんでした。" + }, + { + "id": "app.channel.get_channels_member_count.existing.app_error", + "translation": "指定されたチャンネルのメンバー数が見つかりませんでした。" + }, + { + "id": "app.channel.get_channels_member_count.find.app_error", + "translation": "メンバー数が見つかりませんでした。" + }, + { + "id": "app.channel.update_member.notify_props_limit_exceeded.app_error", + "translation": "チャンネルメンバーを更新できませんでした。notify propsのサイズが制限を超えています。" + }, + { + "id": "app.eport.generate_presigned_url.link.app_error", + "translation": "Presigned URLを生成できませんでした。" + }, + { + "id": "app.eport.generate_presigned_url.config.app_error", + "translation": "この操作には、専用のエクスポート保存先を使用する必要があります。" + }, + { + "id": "app.eport.generate_presigned_url.driver.app_error", + "translation": "お使いのエクスポート保存先ドライバはPresign URLの生成をサポートしていません。" + }, + { + "id": "app.eport.generate_presigned_url.featureflag.app_error", + "translation": "この機能はフィーチャーフラグによって制限されています。" + }, + { + "id": "app.eport.generate_presigned_url.fileexist.app_error", + "translation": "ファイルの存在を確認できませんでした。" + }, + { + "id": "app.eport.generate_presigned_url.notfound.app_error", + "translation": "エクスポートファイルが見つかりませんでした。" + }, + { + "id": "model.channel_member.is_valid.notify_props.app_error", + "translation": "Notify propsのサイズが制限を超えています。" + }, + { + "id": "model.config.is_valid.local_mode_socket.app_error", + "translation": "ローカルソケットファイルのディレクトリが見つかりませんでした。" + }, + { + "id": "app.file_info.get.gif.app_error", + "translation": "GIFをデコードできませんでした。" + }, + { + "id": "api.post.post_priority.priority_post_not_allowed_for_user.request_error", + "translation": "優先度付きの投稿や永続的な通知の作成は許可されていません。" + }, + { + "id": "api.post.post_priority.priority_post_only_allowed_for_root_post.request_error", + "translation": "優先度の設定はrootの投稿にのみ許可されています。" + }, + { + "id": "api.user.invalidate_password_recovery_tokens.error", + "translation": "パスワードリカバリトークンを無効にする際に、タイプ別のトークンを取得できませんでした" + }, + { + "id": "api.user.invalidate_password_recovery_tokens_parse.error", + "translation": "パスワードリカバリトークンを無効化する際にトークンを解析できませんでした" + }, + { + "id": "api.post.post_priority.persistent_notification_validation_error.request_error", + "translation": "永続的な通知の検証に失敗しました。" + }, + { + "id": "model.config.is_valid.elastic_search.ignored_indexes_dash_prefix.app_error", + "translation": "破棄時に無視されるインデックスはダッシュで始まっていてはいけません。" + }, + { + "id": "api.post.post_priority.urgent_persistent_notification_post.request_error", + "translation": "永続的な通知の投稿は、緊急の優先度が設定されている必要があります。" + }, + { + "id": "api.user.invalidate_password_recovery_tokens_delete.error", + "translation": "パスワードリカバリトークンを無効にする際に、トークンを削除できませんでした" + }, + { + "id": "app.channel.update_member.app_error", + "translation": "チャンネルメンバーを更新できませんでした。" + }, + { + "id": "app.save_config.plugin_hook_error", + "translation": "設定保存時のplugin hookを実行する際にエラーが発生しました。" } ] diff --git a/server/i18n/ka.json b/server/i18n/ka.json new file mode 100644 index 00000000000..65fd09570d7 --- /dev/null +++ b/server/i18n/ka.json @@ -0,0 +1,486 @@ +[ + { + "id": "api.command_channel_rename.direct_group.app_error", + "translation": "შეუძლებელია პირდაპირი შეტყობინებების არხების სახელის შეცვლა." + }, + { + "id": "api.command_channel_rename.desc", + "translation": "არხის სახელის შეცვლა" + }, + { + "id": "api.command_channel_rename.channel.app_error", + "translation": "შეცდომა მიმდინარე არხის მიღებისას." + }, + { + "id": "api.command_channel_remove.channel.app_error", + "translation": "შეცდომა არხის მიღებისას." + }, + { + "id": "api.command_channel_purpose.update_channel.app_error", + "translation": "შეცდომა არხის განახლებისას." + }, + { + "id": "api.command_channel_purpose.permission.app_error", + "translation": "თქვენ არ გაქვთ შესაბამისი უფლებები, რომ შეცვალოთ არხის დანიშნულება." + }, + { + "id": "api.command_channel_purpose.name", + "translation": "დანიშნულება" + }, + { + "id": "api.command_channel_purpose.message.app_error", + "translation": "შეტყობინება უნდა იყოს მითითებული /purpose ბრძანებით." + }, + { + "id": "api.command_channel_purpose.hint", + "translation": "[ტექსტი]" + }, + { + "id": "api.command_channel_purpose.direct_group.app_error", + "translation": "დანიშნულება ვერ დაყენდება პირდაპირი შეტყობინების არხებისთვის. ნაცვლად გამოიყენეთ /header რომ დააყენოთ სათაური." + }, + { + "id": "api.command_channel_purpose.desc", + "translation": "არხის დანიშნულების შეცვლა" + }, + { + "id": "api.command.command_post.forbidden.app_error", + "translation": "მითითებული მომხმარებელი არ არის მითითებული არხის წევრი." + }, + { + "id": "api.command.admin_only.app_error", + "translation": "ინტეგრაციებზე მხოლოდ ადმინისტრატორებს აქვთ წვდომა." + }, + { + "id": "api.channel.update_team_member_roles.scheme_role.app_error", + "translation": "მითითებული როლი იმართება სქემის მიერა და ამიტომაც პირდაპირ ვერ მიენიჭება გუნდის წევრს." + }, + { + "id": "api.channel.update_team_member_roles.changing_guest_role.app_error", + "translation": "გუნდის წევრის არასწორი განახლება: თქვენ არ შეგიძლიათ ხელით დაამატოთ ან წაშალოთ სტუმრის როლი." + }, + { + "id": "api.channel.update_channel_scheme.scheme_scope.error", + "translation": "შეუძლებელია არქის სქემის დაყენება, რადგანაც მითითებული სქემა არ არის არხის სქემა." + }, + { + "id": "api.channel.update_channel_scheme.license.error", + "translation": "თქვენს ლიცენზიას არ აქვს არხის სქემის შეცვლის მხარადჭერა" + }, + { + "id": "api.channel.update_channel_privacy.default_channel_error", + "translation": "ნაგულისხმევი არხი არ შეიძლება გადაკეთდეს პრივატულად." + }, + { + "id": "api.channel.update_channel_member_roles.scheme_role.app_error", + "translation": "მითითებული როლი იმართება სქემის მიერა და ამიტომაც ვერ მიენიჭება პირდაპირ არხის წევრს." + }, + { + "id": "api.channel.update_channel_member_roles.guest_and_user.app_error", + "translation": "არხის წევრის არასწორი განახლება: მომხამრებელი უნდა იყოს მომხმარებელი ან სტუმარი და არა ორივე." + }, + { + "id": "api.channel.update_channel_member_roles.changing_guest_role.app_error", + "translation": "არხის წევრის არასწორი განახლება: თქვენ არ შეგიძლიათ ხელით დაამატოთ ან წაშალოთ სტუმრის როლი." + }, + { + "id": "api.channel.update_channel.typechange.app_error", + "translation": "არხის ტიპი ვერ განახლდება." + }, + { + "id": "api.channel.update_channel.tried.app_error", + "translation": "დაფიქსირდა ნაგულისხმევი არხის არასწორი განახლება {{.Channel}}." + }, + { + "id": "api.channel.update_channel.deleted.app_error", + "translation": "არხი დაარქივდა ან წაიშალა." + }, + { + "id": "api.channel.restore_channel.unarchived", + "translation": "{{.Username}} -მა გამოოაარქივა არხი." + }, + { + "id": "api.channel.restore_channel.restored.app_error", + "translation": "შეუძლებელია არხის გამოარქივება. არხი არ არის დაარქივებული." + }, + { + "id": "api.channel.rename_channel.cant_rename_group_messages.app_error", + "translation": "თქვენ არ შეგიძლიათ შეუცვალოთ სახელი ჯგუფის შეტყობინებების არხს." + }, + { + "id": "api.channel.rename_channel.cant_rename_direct_messages.app_error", + "translation": "თქვენ არ შეგიძლიათ შეუცვალოთ სახელი პირდაპირი შეტყობინებების არხს." + }, + { + "id": "api.channel.remove_user_from_channel.app_error", + "translation": "ვერ ვშლი მომხმარებლებს ამ ტიპის არხში." + }, + { + "id": "api.channel.remove_members.denied", + "translation": "ჯგუფის შეზღუდვებიდან გამომდინარე არხისდან წაშლა აკრძალულია შემდეგი მომხმარებლებისთვის: {{ .UserIDs }}" + }, + { + "id": "api.admin.upload_brand_image.parse.app_error", + "translation": "ვერ ხერხდება მრავალნაწილიანი ფორმის პარსინგი." + }, + { + "id": "api.admin.saml.metadata.app_error", + "translation": "შეცდომა დაფიქსირდა სერვისის პროვაიდერის მეტამონაცემების აწყობისას." + }, + { + "id": "api.admin.saml.invalid_xml_missing_ssoservices.app_error", + "translation": "არ არის ნაპოვნი იდენტობის პროვაიდერის SSO სერვისების ნოდი XML-ში." + }, + { + "id": "api.admin.saml.invalid_xml_missing_keydescriptor.app_error", + "translation": "არ არის ნაპოვნი იდენტობის პროვაიდერის გასაღბების დესკრიპტორები XML ფაილში." + }, + { + "id": "api.admin.saml.invalid_xml_missing_idpssodescriptors.app_error", + "translation": "არ არის ნაპოვნი იდენტობის პროვაიდერის SSO დესკრიპტორების ნოდი XML-ში." + }, + { + "id": "api.admin.saml.failure_parse_idp_certificate.app_error", + "translation": "შეცდომა დაფიქსირდა მონაცემების პარსინგისას მეტამონაცემებში, რომელიც მიღებულია იქნა იდენტობის პროვაიდერის მიერ სერტიფიკატისთვის." + }, + { + "id": "api.admin.saml.failure_get_metadata_from_idp.app_error", + "translation": "ვერ იქნა მოღებული მეტამონაცემები იდენტობის პროვაიდერის URL-დან." + }, + { + "id": "api.admin.delete_brand_image.storage.not_found", + "translation": "ვერ წაიშალა ბრენდის გამოსახულება, არ არის ნაპოვნი." + }, + { + "id": "api.admin.add_certificate.saving.app_error", + "translation": "ვერ შეინახა სერტიფიკატის ფაილი." + }, + { + "id": "api.admin.add_certificate.open.app_error", + "translation": "ვერ გაიხსნა სერტიფიკატის ფაილი." + }, + { + "id": "api.admin.add_certificate.no_file.app_error", + "translation": "არ არის ფაილი მოთხოვნაში 'certificate'" + }, + { + "id": "api.admin.add_certificate.array.app_error", + "translation": "არ არის ფაილი მოთხოვნაში 'certificate'." + }, + { + "id": "web.incoming_webhook.user.app_error", + "translation": "მომხმარებელი არ არის ნაპოვნი." + }, + { + "id": "api.channel.remove_member.removed", + "translation": "%v წაიშალა არხიდან." + }, + { + "id": "api.channel.remove_member.group_constrained.app_error", + "translation": "ვერ წაიშალა მომხმარებელი ჯგუფზე შეზღუდული არხიდან." + }, + { + "id": "api.channel.remove_channel_member.type.app_error", + "translation": "ვერ წაიშალა მომხმარებელი არხიდან." + }, + { + "id": "api.channel.remove.default.app_error", + "translation": "ვერ წაიშალა მომხამრებელი ნაგულისხმევი არხიდან {{.Channel}}." + }, + { + "id": "api.channel.post_user_add_remove_message_and_forget.error", + "translation": "ვერ მოხერხდა შესვლა/დატოვების შემოდგომი შეტყობინების გამოქვეყნება" + }, + { + "id": "api.channel.post_update_channel_header_message_and_forget.updated_to", + "translation": "%s-მა შეცვალა არხის სათაური: %s" + }, + { + "id": "api.channel.post_update_channel_header_message_and_forget.updated_from", + "translation": "%s-მა შეცვალა არხის სათაური, %s-დან : %s-ზე" + }, + { + "id": "api.channel.post_update_channel_header_message_and_forget.retrieve_user.error", + "translation": "ვერ მოხერხდა მომხმარებლის მიღება არხის სათაურის განახლებისას" + }, + { + "id": "api.channel.post_update_channel_header_message_and_forget.removed", + "translation": "%s-მა წაშალა არხის სათაური (იყო: %s)" + }, + { + "id": "api.channel.post_update_channel_header_message_and_forget.post.error", + "translation": "ვერ მოხერხდა არხის სათაურის შეცვლის შეტყობინების გამოქვეყნება" + }, + { + "id": "api.channel.post_update_channel_displayname_message_and_forget.updated_from", + "translation": "%s-მა შეცვალა არხის ეკრანული სახელი: %s-დან : %s-ზე" + }, + { + "id": "api.channel.post_update_channel_displayname_message_and_forget.retrieve_user.error", + "translation": "ვერ მოხერხდა მომხმარებლის მიღება არხის ეკრანული სახელის ველის განახლებისას" + }, + { + "id": "api.channel.post_update_channel_displayname_message_and_forget.create_post.error", + "translation": "ვერ მოხერხდა ეკრანული სახელის განახლების შეტყობინების გამოქვეყნება" + }, + { + "id": "api.channel.post_channel_privacy_message.error", + "translation": "ვერ მოხერხდა არხის პრივატულობის განახლების შეტყობინების გამოქვეყნება." + }, + { + "id": "api.channel.patch_update_channel.forbidden.app_error", + "translation": "არხი ვერ განახლდა." + }, + { + "id": "api.channel.patch_channel_moderations.license.error", + "translation": "თქვენს ლიცენზიას არ აქვს არხების მოდერაციის მხარდაჭერა" + }, + { + "id": "api.channel.move_channel.type.invalid", + "translation": "ვერ გადავიტანე პირდაპირ ან ჯგუფის შეტყობინებების არხი" + }, + { + "id": "api.channel.leave.left", + "translation": "%v დატოვა არხი." + }, + { + "id": "api.channel.leave.last_member.app_error", + "translation": "თქვენ მარტო დარჩით, ცადეთ წაშალოთ პრივატული არხი მისი დატოვების ნაცვლად." + }, + { + "id": "api.channel.leave.direct.app_error", + "translation": "ვერ დატოვეთ პირდაპირი შეტყობინების არხი." + }, + { + "id": "api.channel.leave.default.app_error", + "translation": "ვერ დატოვეთ ნაგულისხმევი არხი {{.Channel}}." + }, + { + "id": "api.channel.join_channel.post_and_forget", + "translation": "%v შემოვიდა არხზე." + }, + { + "id": "api.channel.join_channel.permissions.app_error", + "translation": "თქვენ არ გაქვთ შესაბამისი უფლებები." + }, + { + "id": "api.channel.guest_join_channel.post_and_forget", + "translation": "%v შემოვიდა არხზე, როგორც სტუმარი." + }, + { + "id": "api.channel.get_channel_moderations.license.error", + "translation": "თქვენს ლიცენზიას არ აქვს არხების მოდერაციის მხარდაჭერა" + }, + { + "id": "api.channel.delete_channel.type.invalid", + "translation": "ვერ წაიშალა პირდაპირი ან ჯგუფური შეტყობინებების არხები" + }, + { + "id": "api.channel.delete_channel.deleted.app_error", + "translation": "არხი დაარქივებულია ან წაშლილი." + }, + { + "id": "api.channel.delete_channel.cannot.app_error", + "translation": "შეუძლებელია ნაგულისხმევი არხის წაშლა {{.Channel}}." + }, + { + "id": "api.channel.delete_channel.archived", + "translation": "%v-მა დააარქივა არხი." + }, + { + "id": "api.channel.create_group.bad_user.app_error", + "translation": "ერთ-ერთი მითითებული მომხმარებელი არ არსებობს." + }, + { + "id": "api.channel.create_group.bad_size.app_error", + "translation": "ჯგუფის შეტყობინებების არხები უნდა შეიცავდეს არანაკლებ 3 და არაუმეტეს 8 მომხმარებელს." + }, + { + "id": "api.channel.create_direct_channel.invalid_user.app_error", + "translation": "არასწორი მომხმარებლის ID პირდაპირი არხის შექმნისთვის." + }, + { + "id": "api.channel.create_default_channels.town_square", + "translation": "ქალაქის ცენტრი" + }, + { + "id": "api.channel.create_default_channels.off_topic", + "translation": "ოფ-ტოპიკი" + }, + { + "id": "api.channel.create_channel.max_channel_limit.app_error", + "translation": "შეუძლებელია {{.MaxChannelsPerTeam}}-ზე მეტი არხის შექმნა მიმდინარე გუნდისთვის." + }, + { + "id": "api.channel.create_channel.direct_channel.app_error", + "translation": "უნდა გამოიყენოთ createDirectChannel API სერვისი პირდაპირი შეტყობინებების არხის შექმნისთვის." + }, + { + "id": "api.channel.channel_member_counts_by_group.license.error", + "translation": "თქვენს ლიცენზიას არ აქვს ჯგუფების მხარდაჭერა" + }, + { + "id": "api.channel.change_channel_privacy.public_to_private", + "translation": "ეს არხი გადაკეთდა საჯარო არხად." + }, + { + "id": "api.channel.change_channel_privacy.private_to_public", + "translation": "ეს არხი გადაკეთდა საჯარო არხად და მასში შესვლა ნებისმიერი გუნდის წევრს შეუძლია." + }, + { + "id": "api.channel.add_user_to_channel.type.app_error", + "translation": "ვერ ვამატებ მომხმარებელს ამ ტიპის არხში." + }, + { + "id": "api.channel.add_user.to.channel.failed.deleted.app_error", + "translation": "მომხამრებელი ვერ დაემატა არხს იმიტომ, რომ ის ამოღებულია გუნდიდან." + }, + { + "id": "api.channel.add_user.to.channel.failed.app_error", + "translation": "მომხმარებელი ვერ დაემატა არხს." + }, + { + "id": "api.channel.add_members.user_denied", + "translation": "არხის წევრობა აკრძალული აქვთ შემდეგ მომხმარებლებს ჯგუფის შეზღუდვებიდან გამომდინარე: {{ .UserIDs }}" + }, + { + "id": "api.channel.add_members.error", + "translation": "შეცდომა არხის წევრის(ების) დამატებისას." + }, + { + "id": "api.channel.add_member.added", + "translation": "%v დაემატა არხს %v-ის მიერ." + }, + { + "id": "api.channel.add_guest.added", + "translation": "%v დაემატა არხს %v-ის რეკომენდაციით." + }, + { + "id": "api.bot.teams_channels.add_message_mobile", + "translation": "გთხოვ დამამატო გუნდებში და არხებში, რომელშიც გინდა მონაწილეობა მივიღო. ამისთვის გამოიყენე ბრაუზერი ან Mattermost-ის სამაგიდო აპლიკაცია." + }, + { + "id": "api.bot.create_disabled", + "translation": "ბოტების შექმნა გამორთულია." + }, + { + "id": "api.admin.upload_brand_image.too_large.app_error", + "translation": "ფაილი არ აიტვირთა. ფაილი ძალიან დიდია." + }, + { + "id": "api.admin.upload_brand_image.storage.app_error", + "translation": "გამოსახულების ატვირთვა შეუძლებელია. საცავი არ არის კონფიგურირებული." + }, + { + "id": "api.admin.upload_brand_image.no_file.app_error", + "translation": "არ არის ფაილი 'image'-ის ქვეშ მოთხოვნაში." + }, + { + "id": "api.admin.upload_brand_image.array.app_error", + "translation": "ცარიელი მასივი 'image'-ის ქვეშ მოთხოვნაში." + }, + { + "id": "api.admin.test_s3.missing_s3_bucket", + "translation": "საჭიროა S3 Bucket" + }, + { + "id": "api.admin.test_email.subject", + "translation": "Mattermost - ელ-ფოსტის პარამეტრების ტესტირება" + }, + { + "id": "api.admin.test_email.reenter_password", + "translation": "შეიცავალა SMTP სერვერი, პორტი ან მომხმარებლის სახელი. გთხოვთ თავიდან შეიყვანოთ SMTP პაროლი, რომ დატესტოთ შეერთება." + }, + { + "id": "api.admin.test_email.missing_server", + "translation": "საჭიროა SMTP სერვერი" + }, + { + "id": "api.admin.test_email.body", + "translation": "როგორც ჩანს თქვენი Mattermos-ის ელ-ფოსტა კორექტულადაა გამართული!" + }, + { + "id": "api.admin.saml.set_certificate_from_metadata.missing_content_type.app_error", + "translation": "არ არის კონტენტის ტიპი." + }, + { + "id": "api.admin.saml.set_certificate_from_metadata.invalid_content_type.app_error", + "translation": "არასწორი ტიპის შიგთავსი." + }, + { + "id": "api.admin.saml.set_certificate_from_metadata.invalid_body.app_error", + "translation": "არასწორი სერტიფიკატის ტექსტი." + }, + { + "id": "api.admin.saml.not_available.app_error", + "translation": "SAML 2.0 არ არის კონფიგურურებული ან მხარდაჭერილი ამ სერვერზე." + }, + { + "id": "api.admin.saml.failure_save_idp_certificate_file.app_error", + "translation": "ვერ მოხერხდა სერტიფიკატის ფაილის შენახვა." + }, + { + "id": "api.admin.remove_certificate.delete.app_error", + "translation": "დაფიქსირდა შეცდომა სერტიფიკატის წაშლისას." + }, + { + "id": "api.admin.ldap.not_available.app_error", + "translation": "LDAP არ არის ხელმისაწვდომი." + }, + { + "id": "api.admin.get_brand_image.storage.app_error", + "translation": "გამოსახულებების საცავი არ არის დაყენებული." + }, + { + "id": "api.admin.file_read_error", + "translation": "ლოგ ფაილის წაკითხვის შეცდომა." + }, + { + "id": "September", + "translation": "სექტემბერი" + }, + { + "id": "October", + "translation": "ოქტომბერი" + }, + { + "id": "November", + "translation": "ნოემბერი" + }, + { + "id": "May", + "translation": "მაისი" + }, + { + "id": "March", + "translation": "მარტი" + }, + { + "id": "June", + "translation": "ივნისი" + }, + { + "id": "July", + "translation": "ივლისი" + }, + { + "id": "January", + "translation": "იანვარი" + }, + { + "id": "February", + "translation": "თებერვალი" + }, + { + "id": "December", + "translation": "დეკემბერი" + }, + { + "id": "August", + "translation": "აგვისტო" + }, + { + "id": "April", + "translation": "აპრილი" + } +] diff --git a/server/i18n/kk.json b/server/i18n/kk.json new file mode 100644 index 00000000000..0967ef424bc --- /dev/null +++ b/server/i18n/kk.json @@ -0,0 +1 @@ +{} diff --git a/server/i18n/kk_Latn.json b/server/i18n/kk_Latn.json new file mode 100644 index 00000000000..0967ef424bc --- /dev/null +++ b/server/i18n/kk_Latn.json @@ -0,0 +1 @@ +{} diff --git a/server/i18n/km.json b/server/i18n/km.json new file mode 100644 index 00000000000..f2a965d654b --- /dev/null +++ b/server/i18n/km.json @@ -0,0 +1,106 @@ +[ + { + "id": "api.user.upload_profile_user.open.app_error", + "translation": "មិនអាចបើកឯកសារ certificate បាន។" + }, + { + "id": "api.user.upload_profile_user.no_file.app_error", + "translation": "ពុំមានឯកសារនៅក្រោម 'certificate' ក្នុងការស្នើសុំ។" + }, + { + "id": "api.team.set_team_icon.open.app_error", + "translation": "មិនអាចបើកឯកសារ certificate បាន។" + }, + { + "id": "api.team.set_team_icon.no_file.app_error", + "translation": "ពុំមានឯកសារនៅក្រោម 'certificate' ក្នុងការស្នើសុំ។" + }, + { + "id": "api.team.import_team.open.app_error", + "translation": "មិនអាចបើកឯកសារ certificate បាន។" + }, + { + "id": "api.team.import_team.no_file.app_error", + "translation": "ពុំមានឯកសារនៅក្រោម 'certificate' ក្នុងការស្នើសុំ។" + }, + { + "id": "api.license.add_license.open.app_error", + "translation": "មិនអាចបើកឯកសារ certificate បាន។" + }, + { + "id": "api.license.add_license.no_file.app_error", + "translation": "ពុំមានឯកសារនៅក្រោម 'certificate' ក្នុងការស្នើសុំ។" + }, + { + "id": "api.admin.upload_brand_image.no_file.app_error", + "translation": "ពុំមានឯកសារនៅក្រោម 'certificate' ក្នុងការស្នើសុំ។" + }, + { + "id": "api.admin.saml.failure_save_idp_certificate_file.app_error", + "translation": "មិនអាចរក្សាឯកសារ certificate បានទេ។" + }, + { + "id": "api.admin.add_certificate.saving.app_error", + "translation": "មិនអាចរក្សាឯកសារ certificate បានទេ។" + }, + { + "id": "api.admin.add_certificate.open.app_error", + "translation": "មិនអាចបើកឯកសារ certificate បាន។" + }, + { + "id": "api.admin.add_certificate.no_file.app_error", + "translation": "ពុំមានឯកសារនៅក្រោម 'certificate' ក្នុងការស្នើសុំ។" + }, + { + "id": "api.admin.add_certificate.array.app_error", + "translation": "ពុំមានឯកសារនៅក្រោម 'certificate' ក្នុងការស្នើសុំ។" + }, + { + "id": "September", + "translation": "កញ្ញា​" + }, + { + "id": "October", + "translation": "តុលា​" + }, + { + "id": "November", + "translation": "វិច្ឆិកា​" + }, + { + "id": "May", + "translation": "ឧសភា​" + }, + { + "id": "March", + "translation": "មីនា​" + }, + { + "id": "June", + "translation": "មិថុនា​" + }, + { + "id": "July", + "translation": "កក្កដា​" + }, + { + "id": "January", + "translation": "មករា​" + }, + { + "id": "February", + "translation": "កុម្ភៈ​" + }, + { + "id": "December", + "translation": "ធ្នូ​" + }, + { + "id": "August", + "translation": "សីហា​" + }, + { + "id": "April", + "translation": "មេសា​" + } +] diff --git a/server/i18n/ko.json b/server/i18n/ko.json index 97296dcbdb5..527018c72ab 100644 --- a/server/i18n/ko.json +++ b/server/i18n/ko.json @@ -4035,10 +4035,6 @@ "id": "model.emoji.user_id.app_error", "translation": "잘못된 제출자 ID" }, - { - "id": "model.file_info.get.gif.app_error", - "translation": "Could not decode gif." - }, { "id": "model.file_info.is_valid.create_at.app_error", "translation": "read timeout 항목의 허용되지 않는 값입니다." @@ -7967,14 +7963,6 @@ "id": "api.getThreadsForUser.bad_only_params", "translation": "getThreadsForUser의 OnlyThreads와 OnlyTotals 매개변수는 상호 배타적입니다" }, - { - "id": "app.post.get_top_threads_for_team_since.app_error", - "translation": "팀의 상위 글타래를 가져올 수 없습니다." - }, - { - "id": "app.post.get_top_threads_for_user_since.app_error", - "translation": "사용자의 상위 글타래를 가져올 수 없습니다." - }, { "id": "app.user.get_thread_count_for_user.app_error", "translation": "사용자의 글타래 개수를 가져올 수 없습니다." @@ -8399,18 +8387,6 @@ "id": "api.error_set_first_admin_visit_marketplace_status", "translation": "스토어에 초기 관리자 마켓플레이스 방문 상태를 저장하는 동안 오류가 발생했습니다." }, - { - "id": "api.command_templates.unsupported.app_error", - "translation": "이 장치에서는 템플릿 명령이 지원되지 않습니다." - }, - { - "id": "api.command_templates.name", - "translation": "템플릿" - }, - { - "id": "api.command_templates.desc", - "translation": "템플릿에서 만들기 창을 엽니다" - }, { "id": "api.acknowledgement.save.archived_channel.app_error", "translation": "보관된 채널에서는 승인할 수 없습니다." diff --git a/server/i18n/la.json b/server/i18n/la.json new file mode 100644 index 00000000000..0967ef424bc --- /dev/null +++ b/server/i18n/la.json @@ -0,0 +1 @@ +{} diff --git a/server/i18n/lo.json b/server/i18n/lo.json new file mode 100644 index 00000000000..b14f187dc6e --- /dev/null +++ b/server/i18n/lo.json @@ -0,0 +1,6 @@ +[ + { + "id": "api.command.invite_people.name", + "translation": "invite_people" + } +] diff --git a/server/i18n/lt.json b/server/i18n/lt.json new file mode 100644 index 00000000000..0967ef424bc --- /dev/null +++ b/server/i18n/lt.json @@ -0,0 +1 @@ +{} diff --git a/server/i18n/lv.json b/server/i18n/lv.json new file mode 100644 index 00000000000..0967ef424bc --- /dev/null +++ b/server/i18n/lv.json @@ -0,0 +1 @@ +{} diff --git a/server/i18n/mk.json b/server/i18n/mk.json new file mode 100644 index 00000000000..e31b5e2fc8b --- /dev/null +++ b/server/i18n/mk.json @@ -0,0 +1,946 @@ +[ + { + "id": "api.channel.create_default_channels.town_square", + "translation": "Градски Плоштад" + }, + { + "id": "api.channel.add_members.error", + "translation": "Грешка во додавање на кориник/ци на каналот." + }, + { + "id": "api.channel.add_member.added", + "translation": "%v додаден во каналот од %v." + }, + { + "id": "api.channel.add_guest.added", + "translation": "%v додаден во каналот како гостин од %v." + }, + { + "id": "api.admin.test_s3.missing_s3_bucket", + "translation": "S3 Bucket е потребно" + }, + { + "id": "api.admin.saml.failure_save_idp_certificate_file.app_error", + "translation": "Неможе да се сними сертификат фајл." + }, + { + "id": "api.admin.remove_certificate.delete.app_error", + "translation": "Грешка при бришење на сертификатот." + }, + { + "id": "api.admin.ldap.not_available.app_error", + "translation": "LDAP не е достапен." + }, + { + "id": "api.admin.get_brand_image.storage.app_error", + "translation": "Складиштето за слики не е подесено." + }, + { + "id": "api.admin.file_read_error", + "translation": "Грешка во читање на лог фајлот." + }, + { + "id": "api.admin.delete_brand_image.storage.not_found", + "translation": "Неможе да се избрише бренд сликата, неможе да се најде." + }, + { + "id": "api.admin.add_certificate.saving.app_error", + "translation": "Неможеше да се сними сертификат фајл." + }, + { + "id": "api.admin.add_certificate.parseform.app_error", + "translation": "Грешка во парсирање на мултиформско барање" + }, + { + "id": "api.admin.add_certificate.open.app_error", + "translation": "Не може да се отвори сертификат фајл." + }, + { + "id": "api.admin.add_certificate.no_file.app_error", + "translation": "Нема фајл под 'certificate' во барањето." + }, + { + "id": "api.admin.add_certificate.array.app_error", + "translation": "Нема фајл под 'certificate' во барањето." + }, + { + "id": "September", + "translation": "Септември" + }, + { + "id": "October", + "translation": "Октомври" + }, + { + "id": "November", + "translation": "Ноември" + }, + { + "id": "May", + "translation": "Мај" + }, + { + "id": "March", + "translation": "Март" + }, + { + "id": "June", + "translation": "Јуни" + }, + { + "id": "July", + "translation": "Јули" + }, + { + "id": "January", + "translation": "Јануари" + }, + { + "id": "February", + "translation": "Февруари" + }, + { + "id": "December", + "translation": "Декември" + }, + { + "id": "Channels", + "translation": "Канали" + }, + { + "id": "Boards", + "translation": "Табли" + }, + { + "id": "August", + "translation": "Август" + }, + { + "id": "April", + "translation": "Април" + }, + { + "id": "api.bot.create_disabled", + "translation": "Создавањето на ботови е оневозможено." + }, + { + "id": "api.back_to_app", + "translation": "Назад во {{.SiteName}}" + }, + { + "id": "api.admin.upload_brand_image.too_large.app_error", + "translation": "Неможеше да се уплоадира фајлот. Фајлот е преголем." + }, + { + "id": "api.admin.upload_brand_image.storage.app_error", + "translation": "Неможеше да се уплоадира сликата. Складиштето за слики не е конфигурирано." + }, + { + "id": "api.admin.upload_brand_image.no_file.app_error", + "translation": "Нема фајл под 'image' во барањето." + }, + { + "id": "api.admin.upload_brand_image.array.app_error", + "translation": "Празна низа под 'image' во барањето." + }, + { + "id": "api.admin.test_email.subject", + "translation": "Mattermost - Испробување на емаил подесувања" + }, + { + "id": "api.admin.test_email.reenter_password", + "translation": "SMTP серверот, портата, или корисничкото име се сменети. Внесете ја СМТП лозинката за да се испроба коненцијата." + }, + { + "id": "api.admin.test_email.missing_server", + "translation": "SMTP Сервер е потребен" + }, + { + "id": "api.admin.test_email.body", + "translation": "Изгледа дека вашиот Mattermost емаил не е исправно подесен!" + }, + { + "id": "api.admin.saml.set_certificate_from_metadata.missing_content_type.app_error", + "translation": "Недостаток на тип на соддржина." + }, + { + "id": "api.admin.saml.set_certificate_from_metadata.invalid_content_type.app_error", + "translation": "Невалиден тип на содржина." + }, + { + "id": "api.admin.saml.set_certificate_from_metadata.invalid_body.app_error", + "translation": "Невалиден сертификат текст." + }, + { + "id": "api.admin.saml.not_available.app_error", + "translation": "SAML 2.0 не е конфигуриран или поддржан на овој сервер." + }, + { + "id": "api.admin.saml.metadata.app_error", + "translation": "Грешка додека се градеше Service Provider метадата." + }, + { + "id": "api.admin.saml.failure_reset_authdata_to_email.app_error", + "translation": "Неуспешно беше ресетирањето на AuthData полето во Email." + }, + { + "id": "api.admin.saml.failure_parse_idp_certificate.app_error", + "translation": "Неуспех додека се парсираше метадата информацијата добиена од Идентитет Провајдер во сертификат." + }, + { + "id": "api.admin.saml.failure_get_metadata_from_idp.app_error", + "translation": "Неможеше да се приземе метадата од Идентитет Провајдер УРЛ." + }, + { + "id": "model.guest.is_valid.channels.app_error", + "translation": "Невалидни канали." + }, + { + "id": "web.error.unsupported_browser.min_os_version.windows", + "translation": "Виндоус 8.1+" + }, + { + "id": "web.error.unsupported_browser.min_os_version.mac", + "translation": "macOS 11+" + }, + { + "id": "web.error.unsupported_browser.min_browser_version.safari", + "translation": "Верзија 14.1+" + }, + { + "id": "web.error.unsupported_browser.min_browser_version.firefox", + "translation": "Верзија 91+" + }, + { + "id": "web.error.unsupported_browser.min_browser_version.edge", + "translation": "Верзија 95+" + }, + { + "id": "web.error.unsupported_browser.min_browser_version.chrome", + "translation": "Верзија 106+" + }, + { + "id": "web.error.unsupported_browser.system_browser_or", + "translation": "или" + }, + { + "id": "api.command_msg.missing.app_error", + "translation": "Неможе да се најде корисникот." + }, + { + "id": "api.command_msg.hint", + "translation": "@[корисничко име] 'порака'" + }, + { + "id": "api.command_msg.fail.app_error", + "translation": "Се случи грешка при праќањето на порака кон корисникот." + }, + { + "id": "api.command_msg.dm_fail.app_error", + "translation": "Се случи грешка при создавањето на директната порака." + }, + { + "id": "api.command_msg.desc", + "translation": "Прати Директна Порака до корисникот" + }, + { + "id": "api.command_me.name", + "translation": "јас" + }, + { + "id": "api.command_me.hint", + "translation": "[порака]" + }, + { + "id": "api.command_me.desc", + "translation": "Изврши акција" + }, + { + "id": "api.command_logout.name", + "translation": "одлогирање" + }, + { + "id": "api.command_logout.desc", + "translation": "Одлогирај се од Mattermost" + }, + { + "id": "api.command_leave.name", + "translation": "напушти" + }, + { + "id": "api.command_leave.fail.app_error", + "translation": "Се случи грешка при напуштањето на каналот." + }, + { + "id": "api.command_leave.desc", + "translation": "Напушти го тековниот канал" + }, + { + "id": "api.command_kick.name", + "translation": "удри" + }, + { + "id": "api.command_join.name", + "translation": "приклучи се" + }, + { + "id": "model.guest.is_valid.channel.app_error", + "translation": "Невалиден канал." + }, + { + "id": "web.incoming_webhook.user.app_error", + "translation": "Неможеше да се најде корисникот." + }, + { + "id": "web.incoming_webhook.channel.app_error", + "translation": "Неможеше да се најде каналот." + }, + { + "id": "api.command_msg.name", + "translation": "порака" + }, + { + "id": "api.bot.teams_channels.add_message_mobile", + "translation": "Ве молам додадете ме на тимовите и каналите во кои што сакате да учествувам. За да го направите тоа, користете браузер или Mattermost десктоп апликацијата." + }, + { + "id": "api.admin.upload_brand_image.parse.app_error", + "translation": "Неможеше да се парсира повеќеделна форма." + }, + { + "id": "Playbooks", + "translation": "Работни Книги" + }, + { + "id": "api.channel.add_user.to.channel.failed.deleted.app_error", + "translation": "Неуспеа да се додаде корисникот во каналот зашто тие се отстранети од тимот." + }, + { + "id": "api.channel.add_user.to.channel.failed.app_error", + "translation": "Неуспеа да се додаде корисникот во каналот." + }, + { + "id": "api.channel.add_members.user_denied", + "translation": "Членство во каналот е одбиено на следните корисници поради групни ограничувања: {{ .UserIDs }}" + }, + { + "id": "api.admin.saml.invalid_xml_missing_ssoservices.app_error", + "translation": "Недостига Identity Provider SSO Services јазол во XML." + }, + { + "id": "api.admin.saml.invalid_xml_missing_keydescriptor.app_error", + "translation": "Недостига Identity Provider Key Descriptors јазол во XML." + }, + { + "id": "api.admin.saml.invalid_xml_missing_idpssodescriptors.app_error", + "translation": "Недостига Identity Provider SSO Descriptors јазол во XML." + }, + { + "id": "api.channel.channel_member_counts_by_group.license.error", + "translation": "Вашата лиценца не поддржува групи" + }, + { + "id": "api.channel.change_channel_privacy.public_to_private", + "translation": "Овој канал беше променет во Приватен Канал." + }, + { + "id": "api.channel.change_channel_privacy.private_to_public", + "translation": "Овој канал беше променет во Јавен Канал и може да се придружи од сите членови на тимот." + }, + { + "id": "api.channel.add_user_to_channel.type.app_error", + "translation": "Неможе да се додаде корисникот на овој тип на канал." + }, + { + "id": "api.channel.create_channel.direct_channel.team_restricted_error", + "translation": "Директен канал неможе да се креира помеѓу овие корисничи поради тоа што немаат ништо заедничко." + }, + { + "id": "api.channel.create_channel.direct_channel.app_error", + "translation": "Мора да се користи createDirectChannel АПИ сервис за креирање на канали за директни пораки." + }, + { + "id": "api.channel.create_group.bad_user.app_error", + "translation": "Еден од наведените корисници не постои." + }, + { + "id": "api.channel.create_group.bad_size.app_error", + "translation": "Канали за групни пораки мора да имаат најмалце 3 и најмногу 8 корисници." + }, + { + "id": "api.channel.create_direct_channel.invalid_user.app_error", + "translation": "Невалиден кориснички ИД за креација на директен канал." + }, + { + "id": "api.channel.create_default_channels.off_topic", + "translation": "Оф-топик" + }, + { + "id": "api.channel.create_channel.max_channel_limit.app_error", + "translation": "Неможе да се создаде повеќе од {{.MaxChannelsPerTeam}} за тековниот тим." + }, + { + "id": "api.channel.patch_update_channel.forbidden.app_error", + "translation": "Неуспех при ажурирање на каналот." + }, + { + "id": "api.channel.patch_channel_moderations.license.error", + "translation": "Вашата лиценца не поддржува модерација на канали" + }, + { + "id": "api.channel.patch_channel_moderations.cache_invalidation.error", + "translation": "Грешка при поништување на кеш" + }, + { + "id": "api.channel.move_channel.type.invalid", + "translation": "Неможеше да се преместат канали за директни или групни пораки" + }, + { + "id": "api.channel.leave.left", + "translation": "%v го напушти каналот." + }, + { + "id": "api.channel.leave.last_member.app_error", + "translation": "Вие сте единствениот останат член, пробајте да го отстраните Приватниот Канал наместо да го напуштите." + }, + { + "id": "api.channel.leave.direct.app_error", + "translation": "Неможе да се напушти канал за директни пораки." + }, + { + "id": "api.channel.leave.default.app_error", + "translation": "Неможеше да се напушти стандардниот канал {{.Channel}}." + }, + { + "id": "api.channel.join_channel.post_and_forget", + "translation": "%v се приклучи во каналот." + }, + { + "id": "api.channel.join_channel.permissions.app_error", + "translation": "Немате соодветни дозволи." + }, + { + "id": "api.channel.guest_join_channel.post_and_forget", + "translation": "%v се приклучи во каналот како гостин." + }, + { + "id": "api.channel.get_channel_moderations.license.error", + "translation": "Вашата лиценца не поддржува канална модерација" + }, + { + "id": "api.channel.delete_channel.type.invalid", + "translation": "Неможеше да се избришат канали за директни или групни пораки" + }, + { + "id": "api.channel.delete_channel.deleted.app_error", + "translation": "Каналот бил архивиран или избришен." + }, + { + "id": "api.channel.delete_channel.cannot.app_error", + "translation": "Неможеше да се избрише стандарниот канал {{.Channel}}." + }, + { + "id": "api.channel.delete_channel.archived", + "translation": "%v го архивираше каналот." + }, + { + "id": "api.channel.post_update_channel_header_message_and_forget.retrieve_user.error", + "translation": "Неуспеа да се поврати корисникот при ажурирање на заглавието на каналот" + }, + { + "id": "api.channel.post_update_channel_header_message_and_forget.removed", + "translation": "%s го избриша заглавието на каналот (беше: %s)" + }, + { + "id": "api.channel.post_update_channel_header_message_and_forget.post.error", + "translation": "Неуспех при постирање на ажурирана порака за заглавието на каналот" + }, + { + "id": "api.channel.post_update_channel_displayname_message_and_forget.updated_from", + "translation": "%s го ажурираше екранското име од: %s во: %s" + }, + { + "id": "api.channel.post_update_channel_displayname_message_and_forget.retrieve_user.error", + "translation": "Неуспех при повратување на корисник при ажурирање на полето за екранско име на каналот" + }, + { + "id": "api.channel.post_update_channel_displayname_message_and_forget.create_post.error", + "translation": "Неуспех при постирање на ажурирана порака за екранско име" + }, + { + "id": "api.channel.post_channel_privacy_message.error", + "translation": "Неуспех при постирање на ажурирања порака за приватноста на каналот." + }, + { + "id": "api.admin.syncables_error", + "translation": "неуспех при додадање корисници во групни-тимови и груни-канали" + }, + { + "id": "api.channel.remove_member.group_constrained.app_error", + "translation": "Неуспеа да се извади корисникот од каналот ограничен со група." + }, + { + "id": "api.channel.remove_channel_member.type.app_error", + "translation": "Неусепа да се извади корисникот од каналот." + }, + { + "id": "api.channel.remove.default.app_error", + "translation": "Неуспеа да се извади корисникот од стандардниот канал {{.Channel}}." + }, + { + "id": "api.channel.post_user_add_remove_message_and_forget.error", + "translation": "Неуспеа да се постира пораката за приклучување/напуштување" + }, + { + "id": "api.channel.post_update_channel_header_message_and_forget.updated_from", + "translation": "%s го промени заглавието на каналот од: %s во: %s" + }, + { + "id": "api.channel.post_update_channel_header_message_and_forget.updated_to", + "translation": "%s го промени заглавието на каналот во: %s" + }, + { + "id": "api.channel.rename_channel.cant_rename_group_messages.app_error", + "translation": "Не може да се преименува канал за групни пораки." + }, + { + "id": "api.channel.rename_channel.cant_rename_direct_messages.app_error", + "translation": "Не може да се преименува канал за директни пораки." + }, + { + "id": "api.channel.remove_user_from_channel.app_error", + "translation": "Не може да се отстрани корсникот од овој тип на канал." + }, + { + "id": "api.channel.remove_member.removed", + "translation": "%v отстранет од каналот." + }, + { + "id": "api.acknowledgement.save.archived_channel.app_error", + "translation": "Не можете да потврдите во архивиран канал." + }, + { + "id": "api.acknowledgement.delete.deadline.app_error", + "translation": "Не можете да избришете потврда откако 5 минути се поминати." + }, + { + "id": "api.acknowledgement.delete.archived_channel.app_error", + "translation": "Не можете да отстраните потврда во архириван канал." + }, + { + "id": "api.channel.update_channel_member_roles.changing_guest_role.app_error", + "translation": "Невалиден членска промена на каналот. Неможете да ја додавате или отстраните гостинската улога." + }, + { + "id": "api.channel.update_channel.typechange.app_error", + "translation": "Типот на каналот не може да се промени." + }, + { + "id": "api.channel.update_channel.tried.app_error", + "translation": "Проба да се направи невалидно ажурирање на стандардниот канал {{.Channel}}." + }, + { + "id": "api.channel.update_channel.deleted.app_error", + "translation": "Каналот беше архивиран или избришан." + }, + { + "id": "api.channel.restore_channel.unarchived", + "translation": "{{.Username}} го деархивираше каналот." + }, + { + "id": "api.channel.restore_channel.restored.app_error", + "translation": "Не може да се деархивира каналот. Каналот не е архивиран." + }, + { + "id": "api.channel.remove_members.denied", + "translation": "Отстранувањето на членовите на каналот е одбиено на следните корисници поради групни ограничувања: {{ .UserIDs }}" + }, + { + "id": "api.channel.update_channel_scheme.scheme_scope.error", + "translation": "Не можеше да се промени шемата на каналот поради тоа што наведената шема не е шема за канал." + }, + { + "id": "api.channel.update_channel_scheme.license.error", + "translation": "Вашата лиценца не поддржува промена на шема на каналот" + }, + { + "id": "api.channel.update_channel_privacy.default_channel_error", + "translation": "Стандардниот канал не може да се направи приватен." + }, + { + "id": "api.channel.update_channel_member_roles.scheme_role.app_error", + "translation": "Наведената улога е управувана од шема и затоа не може да се примени директно на членот на каналот." + }, + { + "id": "api.channel.update_channel_member_roles.guest_and_user.app_error", + "translation": "Невалидена промена на членовите на каналот: Корисникот мора да биде или гостин или корисник, но не двете." + }, + { + "id": "api.server.start_server.forward80to443.enabled_but_listening_on_wrong_port", + "translation": "Неможе да се препати порта 80 на порта 443 додека се слуша на порта %s: исклучете Forward80To443 ако користите прокси сервер" + }, + { + "id": "api.server.start_server.forward80to443.disabled_while_using_lets_encrypt", + "translation": "Мора да се вклучи Forward80To443 кога се користи LetsEncrypt" + }, + { + "id": "api.server.license_up_for_renewal.error_sending_email", + "translation": "Не успеа да се прати лиценца за имејли за обновување" + }, + { + "id": "api.server.license_up_for_renewal.error_generating_link", + "translation": "Неуспех при генерирање на линк за обновување на лиценцата" + }, + { + "id": "api.scheme.patch_scheme.license.error", + "translation": "Вашата лиценца не поддржува ажурирање на шеми за дозволи" + }, + { + "id": "api.cloud.app_error", + "translation": "Внатрешна грешка при клауд апи барање." + }, + { + "id": "api.channel.update_team_member_roles.scheme_role.app_error", + "translation": "Наведената улога е менаџирана од Шема и поради тоа неможе да се аплицира директно на Член на Тимот." + }, + { + "id": "api.channel.update_team_member_roles.changing_guest_role.app_error", + "translation": "Невалидно ажурирање на член на тимот: Не можете да додадете или отстранете гостинската улога рачно." + }, + { + "id": "api.cloud.teams_limit_reached.create", + "translation": "Не може да се создаде тимот бидејќи се достигна ограничувањето за тимови" + }, + { + "id": "api.cloud.request_error", + "translation": "Грешка при процесирање барањето на CWS." + }, + { + "id": "api.cloud.notify_admin_to_upgrade_error.already_notified", + "translation": "Веќе е известен администраторот" + }, + { + "id": "api.cloud.license_error", + "translation": "Вашата лиценца не поддржува клауд барања." + }, + { + "id": "api.cloud.delinquency_email.missing_email_to_trigger", + "translation": "Недостигаат потребни полиња за да се прати имејлот за деликвенција." + }, + { + "id": "api.cloud.cws_webhook_event_missing_error", + "translation": "Веб кука настанот не беше обработен. Или недостига или не е валиден." + }, + { + "id": "api.command.execute_command.failed.app_error", + "translation": "Наредба со активирач '{{.Trigger}}' не успеа." + }, + { + "id": "api.command.execute_command.create_post_failed.app_error", + "translation": "Наредба '{{.Trigger}}' не успеа да постира одговор. Молиме контактирајте го вашиот Системски Администратор." + }, + { + "id": "api.command.duplicate_trigger.app_error", + "translation": "Овој збор за активирање веќе се користи. Молиме изберете друг збор." + }, + { + "id": "api.command.disabled.app_error", + "translation": "Наредбите се оневозможени од системскиот администратор." + }, + { + "id": "api.command.command_post.forbidden.app_error", + "translation": "Наведениот корисник не е член на наведениот канал." + }, + { + "id": "api.command.admin_only.app_error", + "translation": "Интеграциите се ограничени само за администратори." + }, + { + "id": "api.cloud.teams_limit_reached.restore", + "translation": "Не може да се врати тимот бидејќи ограничувањето за тимови беше достигнато" + }, + { + "id": "api.command.execute_command.format.app_error", + "translation": "Наредбата за зборот за активирање недостасува водечката коса црта" + }, + { + "id": "api.command.execute_command.failed_resp.app_error", + "translation": "Командата со активирач на '{{.Trigger}}' врати одговор {{.Status}}." + }, + { + "id": "api.command.execute_command.failed_empty.app_error", + "translation": "Командата со активирач на '{{.Trigger}}' врати празен одговор." + }, + { + "id": "model.config.is_valid.teammate_name_display.app_error", + "translation": "Невалиден приказ на тимски член. Мора да биде 'full_name', 'nickname_full_name' или 'username'." + }, + { + "id": "api.command_channel_header.name", + "translation": "заглавје" + }, + { + "id": "api.command_channel_header.message.app_error", + "translation": "Текстот мора да биде наведен со командата /header." + }, + { + "id": "api.command_channel_header.hint", + "translation": "[текст]" + }, + { + "id": "api.command_channel_header.desc", + "translation": "Уреди го заглавјето на каналот" + }, + { + "id": "api.command_channel_header.channel.app_error", + "translation": "Грешка при добавување на сегашниот канал." + }, + { + "id": "api.command_away.success", + "translation": "Вие сте сега отсутни" + }, + { + "id": "api.command_away.name", + "translation": "отсутен" + }, + { + "id": "api.command_away.desc", + "translation": "Подесете го вашиот статус како отсутен" + }, + { + "id": "api.command.invite_people.desc", + "translation": "Прати имејл покана до вашиот Mattermost тим" + }, + { + "id": "api.command.execute_command.start.app_error", + "translation": "Не беше најдена команда со активирач." + }, + { + "id": "api.command.execute_command.not_found.app_error", + "translation": "Команда со активирач '{{.Trigger}}' не беше пронајдена. Да пратите порака што почнува со \"/\", пробајте да додадете празно место на почетокот на пораката." + }, + { + "id": "app.channel.get_channels.not_found.app_error", + "translation": "Не беа најдени канали." + }, + { + "id": "app.channel.get_channels.get.app_error", + "translation": "Неможе да се превземат каналите." + }, + { + "id": "app.channel.get_channel_counts.get.app_error", + "translation": "Не може да се превземат бројките на каналот." + }, + { + "id": "app.channel.get_by_scheme.app_error", + "translation": "Не може да се превземат каналите за наведената шема." + }, + { + "id": "app.channel.get_by_name.missing.app_error", + "translation": "Каналот не постои." + }, + { + "id": "app.channel.get_by_name.existing.app_error", + "translation": "Не може да се најдат постојани канали." + }, + { + "id": "app.channel.get_all_direct.app_error", + "translation": "Не може да се превземат сите директни канали." + }, + { + "id": "app.channel.get_all_channels_count.app_error", + "translation": "Не може да се избројат сите канали." + }, + { + "id": "api.command.invite_people.email_off", + "translation": "Имејл не беше подесен, покани не беа испратени" + }, + { + "id": "api.command.invite_people.email_invitations_off", + "translation": "Имејл поканите се исклучени, не беше пратено" + }, + { + "id": "api.command_channel_header.update_channel.app_error", + "translation": "Грешка при ажурирањето на заглавјето на каналот." + }, + { + "id": "api.command_channel_header.permission.app_error", + "translation": "Немате соодветни привилегии за да го уредите заглавјето на каналот." + }, + { + "id": "api.command.team_mismatch.app_error", + "translation": "Не можеше да се ажурираат командите низ тимовите." + }, + { + "id": "api.command.invite_people.sent", + "translation": "Имејл покани испратени" + }, + { + "id": "api.command.invite_people.no_email", + "translation": "Молиме наведете еден или повеќе валидни имејл адреси" + }, + { + "id": "api.command.invite_people.name", + "translation": "покани_луѓе" + }, + { + "id": "api.command.invite_people.invite_off", + "translation": "Создавање на корисници е исклучено на овој сервер, не се пратија покани" + }, + { + "id": "api.command.invite_people.hint", + "translation": "[ime@domen.com.mk ...]" + }, + { + "id": "api.command.invite_people.fail", + "translation": "При праќање на емејл покан(а/и) имаше грешка" + }, + { + "id": "api.command_channel_purpose.desc", + "translation": "Уреди ја целта на каналот" + }, + { + "id": "api.command_channel_purpose.channel.app_error", + "translation": "Грешка при превземање на сегашниот канал." + }, + { + "id": "api.command_channel_header.update_channel.max_length", + "translation": "Внесениот текст надминува лимитот за карактери. Заглабјето на каналот е лимитирано на {{.MaxLength}} карактери." + }, + { + "id": "api.command_channel_remove.channel.app_error", + "translation": "Грешка при преземање на тековниот канал." + }, + { + "id": "api.command_channel_purpose.update_channel.max_length", + "translation": "Внесениот текст го надминува лимитот на карактери. Целта на каналот е лимитирано на {{.MaxLength}} карактери." + }, + { + "id": "api.command_channel_purpose.update_channel.app_error", + "translation": "Грешка при ажурирањето на целта на каналот." + }, + { + "id": "api.command_channel_purpose.permission.app_error", + "translation": "Немате соодветни дозволи за уредување на целта на каналот." + }, + { + "id": "api.command_channel_purpose.name", + "translation": "цел" + }, + { + "id": "api.command_channel_purpose.message.app_error", + "translation": "Пораката мора да е наведена со командата /purpose." + }, + { + "id": "api.command_channel_purpose.hint", + "translation": "[текст]" + }, + { + "id": "api.command_channel_purpose.direct_group.app_error", + "translation": "Не може да се постави цел за каналите за директни пораки. Kористете /header за да го поставите заглавието наместо." + }, + { + "id": "api.command_channel_rename.name", + "translation": "преименувај" + }, + { + "id": "api.command_channel_rename.message.app_error", + "translation": "Порака мора да се наведе со /rename командата." + }, + { + "id": "api.command_channel_rename.hint", + "translation": "[текст]" + }, + { + "id": "api.command_channel_rename.direct_group.app_error", + "translation": "Не може да се преименуваат каналите за директни пораки." + }, + { + "id": "api.command_channel_rename.desc", + "translation": "Преименувај го каналот" + }, + { + "id": "api.command_channel_rename.channel.app_error", + "translation": "Грешка при добавување на сегашниот канал." + }, + { + "id": "api.command_dnd.desc", + "translation": "\"Не вознемирувај\" ги оневозможува известувањата за десктоп и мобилен." + }, + { + "id": "api.command_dnd.success", + "translation": "\"Не вознемирувај\" е вклучено. Нема да добивате известувања за десктоп или мобилен додека не се исклучи \"Не вознемирувај\"." + }, + { + "id": "api.command_dnd.name", + "translation": "нв" + }, + { + "id": "api.command_custom_status.success", + "translation": "Вашиот статус е подесен на „{{.EmojiName}} {{.StatusMessage}}“. Можете да го промените вашиот статус од статус поповерот во насловот на страничната лента на каналот." + }, + { + "id": "api.command_custom_status.name", + "translation": "статус" + }, + { + "id": "api.command_custom_status.desc", + "translation": "Подесете или исчистете го вашиот статус" + }, + { + "id": "api.command_custom_status.clear.success", + "translation": "Вашиот статус е исчистен." + }, + { + "id": "api.command_custom_status.clear.app_error", + "translation": "Грешка при чистење на статусот." + }, + { + "id": "api.command_custom_status.app_error", + "translation": "Грешка при подесување на статусот." + }, + { + "id": "api.command_collapse.success", + "translation": "Линкови со слики сега стандардно се склопуваат" + }, + { + "id": "api.command_collapse.name", + "translation": "склопи" + }, + { + "id": "api.command_collapse.desc", + "translation": "Вклучете автоматско склопување на прегледи на слики" + }, + { + "id": "api.command_code.name", + "translation": "код" + }, + { + "id": "api.command_code.message.app_error", + "translation": "Порака мора да се наведе со /code командата." + }, + { + "id": "api.command_code.hint", + "translation": "[текст]" + }, + { + "id": "api.command_code.desc", + "translation": "Прикажете го текстот како коден блок" + }, + { + "id": "api.command_channel_rename.update_channel.app_error", + "translation": "Грешка при ажурирање на тековниот канал." + }, + { + "id": "api.command_channel_rename.too_short.app_error", + "translation": "Името на каналот мора да биде {{.Length}} или повеќе карактери." + }, + { + "id": "api.command_channel_rename.too_long.app_error", + "translation": "Името на каналот мора да биде {{.Length}} или помалку карактери." + }, + { + "id": "api.command_channel_rename.permission.app_error", + "translation": "Немате соодветни дозволи за преименување на каналот." + } +] diff --git a/server/i18n/ml.json b/server/i18n/ml.json new file mode 100644 index 00000000000..17207346f00 --- /dev/null +++ b/server/i18n/ml.json @@ -0,0 +1,1185 @@ +[ + { + "id": "ent.saml.service_disable.app_error", + "translation": "SAML 2.0 ഈ സെർവറിൽ രേഖപ്പെടുത്തിയിട്ടില്ല, അല്ലെങ്കിൽ സാധ്യമല്ല." + }, + { + "id": "ent.saml.metadata.app_error", + "translation": "സർവീസ് പ്രൊവൈഡർ മെറ്റാഡാറ്റ നിർമ്മിക്കുവ്വാൻ ശ്രമിച്ചപ്പോൾ തടസ്സം നേരിട്ടു." + }, + { + "id": "api.user.upload_profile_user.parse.app_error", + "translation": "multipart form പരിവർത്തനം ചെയ്യുവാൻ സാധിച്ചില്ല." + }, + { + "id": "api.user.upload_profile_user.open.app_error", + "translation": "ഇമേജ് ഫയൽ തുറക്കുവാൻ സാധിച്ചില്ല." + }, + { + "id": "api.user.upload_profile_user.no_file.app_error", + "translation": "അപേക്ഷയിൽ 'ഇമേജ്'ന്റ്റെ അടിയിൽ ഫയൽ കാണ്മാനില്ല." + }, + { + "id": "api.user.upload_profile_user.array.app_error", + "translation": "അപേക്ഷയിൽ 'ഇമേജ്'ന്റ്റെ അടിയിൽ അറേ ശൂന്യമാണ്." + }, + { + "id": "api.user.update_active.permissions.app_error", + "translation": "താങ്കൾക്ക് ആവശ്യമായ അനുമതി ഇല്ല." + }, + { + "id": "api.user.saml.not_available.app_error", + "translation": "SAML 2.0 ഈ സെർവറിൽ രേഖപ്പെടുത്തിയിട്ടില്ല, അല്ലെങ്കിൽ സാധ്യമല്ല." + }, + { + "id": "api.team.set_team_icon.parse.app_error", + "translation": "multipart form പരിവർത്തനം ചെയ്യുവാൻ സാധിച്ചില്ല." + }, + { + "id": "api.team.set_team_icon.open.app_error", + "translation": "ഇമേജ് ഫയൽ തുറക്കുവാൻ സാധിച്ചില്ല." + }, + { + "id": "api.team.set_team_icon.no_file.app_error", + "translation": "അപേക്ഷയിൽ 'ഇമേജ്'ന്റ്റെ അടിയിൽ ഫയൽ കാണ്മാനില്ല." + }, + { + "id": "api.team.set_team_icon.array.app_error", + "translation": "അപേക്ഷയിൽ 'ഇമേജ്'ന്റ്റെ അടിയിൽ അറേ ശൂന്യമാണ്." + }, + { + "id": "api.team.import_team.parse.app_error", + "translation": "multipart form പരിവർത്തനം ചെയ്യുവാൻ സാധിച്ചില്ല." + }, + { + "id": "api.context.permissions.app_error", + "translation": "താങ്കൾക്ക് ആവശ്യമായ അനുമതി ഇല്ല." + }, + { + "id": "api.channel.update_channel.deleted.app_error", + "translation": "ചാനൽ ആർക്കൈവ് അല്ലെങ്കിൽ നീക്കം ചെയ്തിരിക്കുന്നു." + }, + { + "id": "api.channel.rename_channel.cant_rename_group_messages.app_error", + "translation": "ഗ്രൂപ്പ് മെസ്സേജ് ചാനലിന്റ്റെ പേര് മാറ്റൽ അനവദനീയമല്ല." + }, + { + "id": "api.channel.rename_channel.cant_rename_direct_messages.app_error", + "translation": "ഡയറക്റ്റ് മെസ്സേജ് ചാനലിന്റ്റെ പേര് മാറ്റൽ അനുവദനീയമല്ല." + }, + { + "id": "api.channel.remove_user_from_channel.app_error", + "translation": "ഈ ചാനൽ ഇനത്തിൽ നിന്ന് യൂസറെ നീക്കം ചെയ്യുവാൻ സാധ്യമല്ല." + }, + { + "id": "api.channel.remove_members.denied", + "translation": "ഗ്രൂപ്പ് നിബന്ധനകൾ ഉള്ളതിനാൽ {{ .UserIDs }} കളുടെ ചാനൽ അംഗത്വം നീക്കം ചെയ്യുവാൻ സാധ്യമല്ല" + }, + { + "id": "api.channel.remove_member.removed", + "translation": "ചാനലിൽ നിന്ന് %v നീക്കം ചെയ്യപ്പെട്ടിരിക്കുന്നു." + }, + { + "id": "api.channel.remove_member.group_constrained.app_error", + "translation": "ഗ്രൂപ്പ് നിബന്ധനകൾ ഉള്ള ചാനലിൽ നിന്ന് യൂസറെ നീക്കം ചെയ്യുവാൻ സാധ്യമല്ല." + }, + { + "id": "api.channel.remove_channel_member.type.app_error", + "translation": "ചാനലിൽ നിന്ന് യൂസറെ നീക്കം ചെയ്യുവാൻ സാധ്യമല്ല." + }, + { + "id": "api.channel.remove.default.app_error", + "translation": "{{.Channel}} ഡീഫോൾട് ചാനലിൽ നിന്ന് യൂസറെ നീക്കം ചെയ്യുവാൻ സാധ്യമല്ല." + }, + { + "id": "api.channel.post_user_add_remove_message_and_forget.error", + "translation": "ചേർന്നു/പിന്മാറി വിവരങ്ങൾ പോസ്റ്റ് ചെയ്യുവാൻ സാധിച്ചില്ല" + }, + { + "id": "api.channel.post_update_channel_header_message_and_forget.updated_to", + "translation": "%s ചാനലിന്റെ തലക്കെട്ടു വിവരങ്ങൾ %s ലേക്ക് പുതുക്കിയിരിക്കുന്നു" + }, + { + "id": "api.channel.post_update_channel_header_message_and_forget.updated_from", + "translation": "%s ചാനലിന്റെ തലക്കെട്ടു വിവരങ്ങൾ %s ൽ നിന്ന് %s ലേക്ക് മാറ്റിയിരിക്കുന്നു" + }, + { + "id": "api.channel.post_update_channel_header_message_and_forget.retrieve_user.error", + "translation": "ചാനലിന്റെ തലക്കെട്ടു വിവരങ്ങൾ പുതുക്കുന്നതിനിടക്ക് യുസർ വിവരങ്ങൾ കണ്ടെടുക്കുവാൻ സാധിച്ചില്ല" + }, + { + "id": "api.channel.post_update_channel_header_message_and_forget.removed", + "translation": "%s ചാനലിന്റെ തലക്കെട്ടു വിവരങ്ങൾ നീക്കം ചെയ്തിരിക്കുന്നു (മുൻപ് %s എന്ന് ആയിരുന്നു)" + }, + { + "id": "api.channel.post_update_channel_header_message_and_forget.post.error", + "translation": "ചാനലിന്റെ പുതുക്കിയ തലക്കെട്ടു വിവരങ്ങൾ പോസ്റ്റ് ചെയ്യുവാൻ സാധിച്ചില്ല" + }, + { + "id": "api.channel.post_update_channel_displayname_message_and_forget.updated_from", + "translation": "%s ചാനലിന്റെ ഡിസ്പ്ലേ നെയിം %s ൽ നിന്ന് %s ലേക്ക് മാറ്റിയിരിക്കുന്നു" + }, + { + "id": "api.channel.post_update_channel_displayname_message_and_forget.retrieve_user.error", + "translation": "ചാനൽ DisplayName പുതുക്കുമ്പോൾ യുസർ വിവരങ്ങൾ കണ്ടെടുക്കുവാൻ സാധിച്ചില്ല" + }, + { + "id": "api.channel.post_update_channel_displayname_message_and_forget.create_post.error", + "translation": "ഡിസ്പ്ലേ നെയിം പുതുക്കിയ അറിയിപ്പ് പോസ്റ്റ് ചെയ്യുവാൻ സാധിച്ചില്ല" + }, + { + "id": "api.channel.post_channel_privacy_message.error", + "translation": "ചാനൽ സ്വകാര്യത സൂക്തങ്ങൾ പുതുക്കുവാൻ സാധിച്ചില്ല." + }, + { + "id": "api.channel.patch_update_channel.forbidden.app_error", + "translation": "ചാനൽ പുതുക്കുവാൻ സാധിച്ചില്ല." + }, + { + "id": "api.channel.patch_channel_moderations.license.error", + "translation": "താങ്കളുടെ ലൈസൻസ് ചാനൽ മധ്യസ്ഥ അനുവദിക്കുന്നതല്ല" + }, + { + "id": "api.channel.leave.left", + "translation": "%v ചാനലിൽ നിന്ന് പിന്മാറിയിരിക്കുന്നു." + }, + { + "id": "api.channel.leave.last_member.app_error", + "translation": "താങ്കൾ മാത്രാമാണ് ബാക്കിയുള്ളത്, ദയവായി പിന്മാറുന്നതിനു പകരം സ്വകാര്യ ചാനൽ നീക്കം ചെയ്യാൻ ശ്രമിക്കുക." + }, + { + "id": "api.channel.leave.direct.app_error", + "translation": "ഡയറക്റ്റ് മെസ്സേജ് ചാനലിൽ നിന്ന് പിന്മാറുവാൻ സാധ്യമല്ല." + }, + { + "id": "api.channel.leave.default.app_error", + "translation": "{{.Channel}} എന്ന ഡീഫോൾട് ചാനലിൽ നിന്ന് പിന്മാറുവാൻ സാധ്യമല്ല." + }, + { + "id": "api.channel.join_channel.post_and_forget", + "translation": "%v ചാനലിൽ ചേർന്നിരിക്കുന്നു." + }, + { + "id": "api.channel.join_channel.permissions.app_error", + "translation": "താങ്കൾക്ക് ആവശ്യമായ അനുമതി ഇല്ല." + }, + { + "id": "api.channel.guest_join_channel.post_and_forget", + "translation": "%v ചാനലിൽ വിരുന്നുകാരനായി ചേർന്നിരിക്കുന്നു." + }, + { + "id": "api.channel.get_channel_moderations.license.error", + "translation": "താങ്കളുടെ ലൈസൻസ് ചാനൽ മധ്യസ്ഥ അനുവദിക്കുന്നതല്ല" + }, + { + "id": "api.channel.delete_channel.type.invalid", + "translation": "ഡയറക്റ്റ് അല്ലെങ്കിൽ ഗ്രൂപ്പ് മെസ്സേജ് ചാനലുകൾ നശിപ്പിക്കുവാൻ സാധ്യമല്ല" + }, + { + "id": "api.channel.delete_channel.deleted.app_error", + "translation": "ചാനൽ ആർക്കൈവ് അല്ലെങ്കിൽ നീക്കം ചെയ്തിരിക്കുന്നു." + }, + { + "id": "api.channel.delete_channel.cannot.app_error", + "translation": "{{.Channel}} എന്ന ഡീഫോൾട് ചാനൽ നശിപ്പിക്കുവാൻ സാധ്യമല്ല." + }, + { + "id": "api.channel.delete_channel.archived", + "translation": "%v ചാനൽ ആർക്കൈവ് ചെയ്തു." + }, + { + "id": "api.channel.create_group.bad_user.app_error", + "translation": "നൽകിയ യൂസറുകളിൽ ഒന്ന് കണ്ടെത്തുവാൻ സാധിച്ചില്ല." + }, + { + "id": "api.channel.create_group.bad_size.app_error", + "translation": "ഗ്രൂപ്പ് മെസ്സേജ് ചാനലുകളിൽ മൂന്നിൽ അധികം പക്ഷെ എട്ടിൽ കുറവ് അംഗങ്ങൾ മാത്രം." + }, + { + "id": "api.channel.create_direct_channel.invalid_user.app_error", + "translation": "ഡയറക്റ്റ് ചാനൽ സൃഷ്ടിക്കുവാൻ ഉപയോഗിച്ചത് തെറ്റായ യുസർ ഐഡി ആണ്." + }, + { + "id": "api.channel.create_default_channels.town_square", + "translation": "നടുമുറ്റം" + }, + { + "id": "api.channel.create_default_channels.off_topic", + "translation": "വിഷയേതരം" + }, + { + "id": "api.channel.create_channel.max_channel_limit.app_error", + "translation": "ഇപ്പോഴത്തെ ടീമിൽ {{.MaxChannelsPerTeam}} ൽ അധികം ചാനൽ സൃഷ്ടിക്കുവാൻ സാധ്യമല്ല." + }, + { + "id": "api.channel.create_channel.direct_channel.app_error", + "translation": "ഡയറക്റ്റ് മെസ്സേജ് ചാനൽ സൃഷ്ടിക്കുവാൻ createDirectChannel API സർവ്വീസ് നിർബന്ധമായി ഉപയോഗിക്കുക." + }, + { + "id": "api.channel.channel_member_counts_by_group.license.error", + "translation": "നിങ്ങളുടെ ലൈസൻസിന് ഗ്രൂപ്പുകൾ അനുവദിച്ചിട്ടില്ല" + }, + { + "id": "api.channel.change_channel_privacy.public_to_private", + "translation": "ഈ ചാനൽ ഒരു സ്വകാര്യ ചാനൽ ആക്കി മാറ്റിയിരിക്കുന്നു." + }, + { + "id": "api.channel.change_channel_privacy.private_to_public", + "translation": "ഈ ചാനൽ ഒരു പൊതു ചാനലിലേക്ക് മാറ്റിയിരിക്കുന്നു. ഏതു ടീം അംഗത്തിനും ഇതിൽ ഇനി ചേരുവാൻ സാധിക്കും." + }, + { + "id": "api.channel.add_user_to_channel.type.app_error", + "translation": "യൂസറെ ഈ തരത്തിൽ ഉള്ള ചാനലിലേക്കു ചേർക്കുവ്വാൻ സാധ്യം അല്ല." + }, + { + "id": "api.channel.add_user.to.channel.failed.deleted.app_error", + "translation": "യൂസറെ ടീമിൽ നിന്ന് നീക്കം ചെയ്തതിനാൽ ചാനലിലേക്കു ചേർക്കുവാൻ സാധിച്ചില്ല." + }, + { + "id": "api.channel.add_user.to.channel.failed.app_error", + "translation": "യൂസറെ ചാനലിലേക്കു ചേർക്കുവാൻ സാധിച്ചില്ല." + }, + { + "id": "api.channel.add_members.user_denied", + "translation": "ഗ്രൂപ്പ് നിബന്ധനകൾ കാരണം ഈ പറയുന്ന അംഗങ്ങൾക്ക് ചാനൽ അംഗത്വം നിഷേധിച്ചിരിക്കുന്നു: {{ .UserIDs }}" + }, + { + "id": "api.channel.add_members.error", + "translation": "ചാനൽ അംഗങ്ങളെ ചേർക്കുമ്പോൾ തടസ്സം നേരിട്ടിരുന്നു." + }, + { + "id": "api.channel.add_member.added", + "translation": "%v യെ ചാനലിലേക്കു %v ചേർത്തിരിക്കുന്നു." + }, + { + "id": "api.channel.add_guest.added", + "translation": "%v ചാനലിലേക്ക് അതിഥിയായി %v ചേർത്തു." + }, + { + "id": "api.bot.teams_channels.add_message_mobile", + "translation": "ദയാവായി എന്നെ നിങ്ങൾ സംവദിക്കുന്ന ടീമുകളിലും ചാനലുകളിലും ചേർക്കുക. ഇത് ചെയ്യുവാൻ ബ്രൗസർ അല്ലെങ്കിൽ മാറ്റെർമോസ്റ്റ് ഡെസ്ക്ടോപ്പ് ആപ്പ്ളിക്കേഷൻ ഉപയോഗിക്കുക." + }, + { + "id": "api.bot.create_disabled", + "translation": "ബോട്ട് നിർമ്മിതി അനുവദിച്ചിട്ടില്ല." + }, + { + "id": "api.admin.upload_brand_image.too_large.app_error", + "translation": "ഫയൽ വളരെ വലുതായതിനാൽ അപ്‌ലോഡ് ചെയ്യുവാൻ സാധിച്ചില്ല." + }, + { + "id": "api.admin.upload_brand_image.storage.app_error", + "translation": "ഇമേജ് അപ്‌ലോഡ് ചെയ്യുവാൻ സാധിച്ചില്ല. പടങ്ങളുടെ സൂക്ഷിപ്പ് സ്‌ഥലം സജ്ജീകരിച്ചിട്ടില്ല." + }, + { + "id": "api.admin.upload_brand_image.parse.app_error", + "translation": "multipart form പരിവർത്തനം ചെയ്യുവാൻ സാധിച്ചില്ല." + }, + { + "id": "api.admin.upload_brand_image.no_file.app_error", + "translation": "അപേക്ഷയിൽ 'ഇമേജ്'ന്റ്റെ അടിയിൽ ഫയൽ കാണ്മാനില്ല." + }, + { + "id": "api.admin.upload_brand_image.array.app_error", + "translation": "അപേക്ഷയിൽ 'ഇമേജ്'ന്റ്റെ അടിയിൽ അറേ ശൂന്യമാണ്." + }, + { + "id": "api.admin.test_s3.missing_s3_bucket", + "translation": "S3 ബക്കറ്റ് ആവശ്യമാണ്" + }, + { + "id": "api.admin.test_email.subject", + "translation": "മാറ്റെർമോസ്റ് - ഈമെയിൽ സജ്ജീകരണങ്ങൾ ടെസ്റ്റ് ചെയ്യുന്നു" + }, + { + "id": "api.admin.test_email.reenter_password", + "translation": "SMTP സർവറുടെ നാമം, പോർട്ട്, അല്ലെങ്കിൽ യൂസർനെയിം മാറ്റിയിരിക്കുന്നു. കണക്ഷൻ ടെസ്റ്റ് ചെയ്യുവാനായി ദയവായി SMTP പാസ്സ്‌വേർഡ് രേഖപ്പെടുത്തുക." + }, + { + "id": "api.admin.test_email.missing_server", + "translation": "SMTP സർവർ ആവശ്യമാണ്" + }, + { + "id": "api.admin.test_email.body", + "translation": "താങ്കളുടെ മാറ്റെർമോസ്റ്റ് ഈമെയിൽ ശെരിയായി സജ്ജീകരിച്ചിരിക്കുന്നു!" + }, + { + "id": "api.admin.saml.set_certificate_from_metadata.missing_content_type.app_error", + "translation": "കണ്ടെന്റ് ടൈപ്പ് കാണ്മാനില്ല." + }, + { + "id": "api.admin.saml.set_certificate_from_metadata.invalid_content_type.app_error", + "translation": "അസാധുവായ കണ്ടെന്റ് ടൈപ്പ്." + }, + { + "id": "api.admin.saml.set_certificate_from_metadata.invalid_body.app_error", + "translation": "അസാധുവായ സർട്ടിഫിക്കറ്റ് ടെക്സ്റ്റ്." + }, + { + "id": "api.admin.saml.not_available.app_error", + "translation": "SAML 2.0 ഈ സെർവറിൽ രേഖപ്പെടുത്തിയിട്ടില്ല, അല്ലെങ്കിൽ സാധ്യമല്ല." + }, + { + "id": "api.admin.saml.metadata.app_error", + "translation": "സർവീസ് പ്രൊവൈഡർ മെറ്റാഡാറ്റ നിർമ്മിക്കുവ്വാൻ ശ്രമിച്ചപ്പോൾ തടസ്സം നേരിട്ടു." + }, + { + "id": "api.admin.saml.invalid_xml_missing_ssoservices.app_error", + "translation": "XML ൽ ഐഡന്റിറ്റി പ്രൊവൈഡർ SSO സർവീസെസ് നോട് കണ്ടെത്തിയില്ല." + }, + { + "id": "api.admin.saml.invalid_xml_missing_keydescriptor.app_error", + "translation": "XML ൽ ഐഡന്റിറ്റി പ്രൊവൈഡർ കീ ഡിസ്ക്രിപ്റ്റേഴ്സ് നോട് കണ്ടെത്തിയില്ല." + }, + { + "id": "api.admin.saml.invalid_xml_missing_idpssodescriptors.app_error", + "translation": "XML ൽ ഐഡന്റിറ്റി പ്രൊവൈഡർ SSO ഡിസ്ക്രിപ്റ്റേഴ്സ് നോട് കണ്ടെത്തിയില്ല." + }, + { + "id": "api.admin.saml.failure_save_idp_certificate_file.app_error", + "translation": "സർട്ടിഫിക്കറ്റ് ഫയൽ സേവ് ചെയ്യുവാൻ സാധിച്ചില്ല." + }, + { + "id": "api.admin.saml.failure_parse_idp_certificate.app_error", + "translation": "ഐഡന്റിറ്റി പ്രൊവൈഡറിൽ നിന്നും ലഭിച്ച മെറ്റാഡാറ്റ പരിവർത്തനം ചെയ്ത് സർട്ടിഫിക്കറ്റിലേക്കു രൂപാന്തരപ്പെടുത്തുവാൻ ശ്രമിച്ചപ്പോൾ തടസ്സം നേരിട്ടു." + }, + { + "id": "api.admin.saml.failure_get_metadata_from_idp.app_error", + "translation": "ഐഡന്റിറ്റി പ്രൊവൈഡർ URL ൽ നിന്നും മെറ്റാഡാറ്റ കൈപറ്റുവാൻ സാധിച്ചില്ല." + }, + { + "id": "api.admin.remove_certificate.delete.app_error", + "translation": "സർട്ടിഫിക്കറ്റ് നശിപ്പിക്കുവാൻ ശ്രമിച്ചപ്പോൾ തടസ്സം നേരിട്ടു." + }, + { + "id": "api.admin.get_brand_image.storage.app_error", + "translation": "പടങ്ങൾ സൂക്ഷിക്കുവാനുള്ള ഇടം രേഖപ്പെടുത്തിയിട്ടില്ല." + }, + { + "id": "api.admin.file_read_error", + "translation": "ലോഗ് ഫയൽ വായിക്കുവാൻ കഴിഞ്ഞില്ല." + }, + { + "id": "api.admin.delete_brand_image.storage.not_found", + "translation": "വാണിജ്യമുദ്ര നശിപ്പിക്കുവാൻ സാധിച്ചില്ല, കണ്ടെത്തുവാൻ കഴിഞ്ഞില്ല." + }, + { + "id": "api.admin.add_certificate.saving.app_error", + "translation": "സർട്ടിഫിക്കറ്റ് ഫയൽ സേവ് ചെയ്യുവാൻ സാധിച്ചില്ല." + }, + { + "id": "api.admin.add_certificate.open.app_error", + "translation": "സർട്ടിഫിക്കറ്റ് ഫയൽ തുറക്കുവാൻ സാധിച്ചില്ല." + }, + { + "id": "api.admin.add_certificate.no_file.app_error", + "translation": "അപേക്ഷയിൽ 'സർട്ടിഫിക്കറ്റിന്റ്റെ' അടിയിൽ ഫയൽ ഇല്ല." + }, + { + "id": "api.admin.add_certificate.array.app_error", + "translation": "അപേക്ഷയിൽ 'സർട്ടിഫിക്കറ്റിന്റ്റെ' അടിയിൽ ഫയൽ ഇല്ല." + }, + { + "id": "September", + "translation": "സെപ്റ്റംബർ" + }, + { + "id": "October", + "translation": "ഒക്ടോബർ" + }, + { + "id": "November", + "translation": "നവംബർ" + }, + { + "id": "May", + "translation": "മെയ്" + }, + { + "id": "March", + "translation": "മാർച്ച്" + }, + { + "id": "June", + "translation": "ജൂൺ" + }, + { + "id": "July", + "translation": "ജൂലൈ" + }, + { + "id": "January", + "translation": "ജനുവരി" + }, + { + "id": "February", + "translation": "ഫെബ്രുവരി" + }, + { + "id": "December", + "translation": "ഡിസംബർ" + }, + { + "id": "August", + "translation": "ആഗസ്റ്റ്" + }, + { + "id": "April", + "translation": "ഏപ്രിൽ" + }, + { + "id": "api.channel.update_channel_scheme.license.error", + "translation": "നിങ്ങളുടെ ലൈസൻസ് ഒരു ചാനലിന്റെ സ്കീം അപ്ഡേറ്റ് ചെയ്യുന്നതിനെ പിന്തുണയ്ക്കുന്നില്ല" + }, + { + "id": "api.channel.update_channel_privacy.default_channel_error", + "translation": "സ്ഥിര ചാനൽ സ്വകാര്യമാക്കാനാകില്ല." + }, + { + "id": "api.channel.update_channel.typechange.app_error", + "translation": "ചാനൽ തരം പരിഷ്‌കരിക്കാൻ കഴിയില്ല." + }, + { + "id": "api.channel.restore_channel.restored.app_error", + "translation": "ചാനൽ ആർക്കൈവുചെയ്യാനാകുന്നില്ല. ചാനൽ ആർക്കൈവുചെയ്‌തിട്ടില്ല." + }, + { + "id": "api.channel.patch_channel_moderations.cache_invalidation.error", + "translation": "കാഷെ അസാധുവാക്കുന്നതിൽ പിശക്" + }, + { + "id": "api.channel.move_channel.type.invalid", + "translation": "നേരിട്ടുള്ള അല്ലെങ്കിൽ ഗ്രൂപ്പ് സന്ദേശ ചാനലുകൾ നീക്കാൻ കഴിയില്ല" + }, + { + "id": "api.back_to_app", + "translation": "തിരികെ {{.SiteName}}" + }, + { + "id": "api.channel.create_channel.direct_channel.team_restricted_error", + "translation": "ഈ ഉപയോക്താക്കൾക്കിടയിൽ ഒരു നേരിട്ടുള്ള ചാനൽ സൃഷ്ടിക്കാൻ കഴിയില്ല, കാരണം അവർ പൊതുവായി ഒരു ടീം പങ്കിടുന്നില്ല." + }, + { + "id": "api.admin.add_certificate.parseform.app_error", + "translation": "വ്യത്യസ്‌തരൂപത്തിലുള്ള അപേക്ഷകൾ വിശകലനം ചെയ്യുവാൻ സാധിച്ചില്ല" + }, + { + "id": "api.admin.saml.failure_reset_authdata_to_email.app_error", + "translation": "AuthData ഫീൽഡ് ഇമെയിലിലേക്ക് പുനഃക്രമീകരിക്കുന്നതിൽ പരാജയപ്പെട്ടു." + }, + { + "id": "api.admin.ldap.not_available.app_error", + "translation": "LDAP ലഭ്യമല്ല." + }, + { + "id": "api.cloud.license_error", + "translation": "നിങ്ങളുടെ അനുമതിപത്രം ക്ലൗഡ് അഭ്യർത്ഥനകളെ പിന്തുണയ്ക്കുന്നില്ല." + }, + { + "id": "api.command_away.name", + "translation": "ദൂരെ" + }, + { + "id": "api.command_away.desc", + "translation": "നിങ്ങളുടെ സ്റ്റാറ്റസ് ക്രമീകരിക്കുക" + }, + { + "id": "api.command_channel_header.desc", + "translation": "ചാനൽ തലക്കെട്ട് എഡിറ്റ് ചെയ്യുക" + }, + { + "id": "api.command_channel_header.channel.app_error", + "translation": "നിലവിലെ ചാനൽ വീണ്ടെടുക്കുന്നതിൽ പിശക്." + }, + { + "id": "api.command_away.success", + "translation": "നിങ്ങൾ ഇപ്പോൾ അകലെയാണ്" + }, + { + "id": "api.channel.update_team_member_roles.scheme_role.app_error", + "translation": "നൽകിയിരിക്കുന്ന റോൾ നിയന്ത്രിക്കുന്നത് ഒരു സ്കീമാണ്, അതിനാൽ ഒരു ടീം അംഗത്തിന് നേരിട്ട് പ്രയോഗിക്കാൻ കഴിയില്ല." + }, + { + "id": "api.channel.update_team_member_roles.changing_guest_role.app_error", + "translation": "ടീം അംഗത്തിന്റെ അപ്‌ഡേറ്റ് അസാധുവാണ്: നിങ്ങൾക്ക് അതിഥി വേഷം സ്വമേധയാ ചേർക്കാനോ നീക്കം ചെയ്യാനോ കഴിയില്ല." + }, + { + "id": "api.channel.update_channel_scheme.scheme_scope.error", + "translation": "വിതരണം ചെയ്ത സ്കീം ഒരു ചാനൽ സ്കീം അല്ലാത്തതിനാൽ ചാനലിലേക്ക് സ്കീം സജ്ജീകരിക്കാനായില്ല." + }, + { + "id": "api.channel.update_channel_member_roles.scheme_role.app_error", + "translation": "നൽകിയിരിക്കുന്ന റോൾ നിയന്ത്രിക്കുന്നത് ഒരു സ്കീമാണ്, അതിനാൽ ഒരു ചാനൽ അംഗത്തിന് നേരിട്ട് പ്രയോഗിക്കാൻ കഴിയില്ല." + }, + { + "id": "api.channel.update_channel_member_roles.guest_and_user.app_error", + "translation": "ചാനൽ അംഗത്തിന്റെ അപ്‌ഡേറ്റ് അസാധുവാണ്: ഒരു ഉപയോക്താവ് അതിഥിയോ ഉപയോക്താവോ ആയിരിക്കണം എന്നാൽ രണ്ടും അല്ല." + }, + { + "id": "api.channel.update_channel_member_roles.changing_guest_role.app_error", + "translation": "അസാധുവായ ചാനൽ അംഗം അപ്‌ഡേറ്റ്: നിങ്ങൾക്ക് അതിഥി റോൾ സ്വമേധയാ ചേർക്കാനോ നീക്കംചെയ്യാനോ കഴിയില്ല." + }, + { + "id": "api.command_channel_rename.permission.app_error", + "translation": "ചാനലിന്റെ പേരുമാറ്റാൻ നിങ്ങൾക്ക് ഉചിതമായ അനുമതികൾ ഇല്ല." + }, + { + "id": "api.command_channel_rename.name", + "translation": "പേരുമാറ്റുക" + }, + { + "id": "api.command_channel_rename.message.app_error", + "translation": "/rename കമാൻഡ് ഉപയോഗിച്ച് ഒരു സന്ദേശം നൽകണം." + }, + { + "id": "api.command_channel_rename.hint", + "translation": "[വാചകം]" + }, + { + "id": "api.command_channel_rename.direct_group.app_error", + "translation": "നേരിട്ടുള്ള സന്ദേശ ചാനലുകളുടെ പേരുമാറ്റാൻ കഴിയുന്നില്ല." + }, + { + "id": "api.command_channel_rename.desc", + "translation": "ചാനലിന്റെ പേര് മാറ്റുക" + }, + { + "id": "api.command_channel_rename.channel.app_error", + "translation": "നിലവിലെ ചാനൽ വീണ്ടെടുക്കുന്നതിൽ പിശക്." + }, + { + "id": "api.command_channel_remove.channel.app_error", + "translation": "നിലവിലെ ചാനൽ വീണ്ടെടുക്കുന്നതിൽ പിശക്." + }, + { + "id": "api.command_channel_purpose.update_channel.max_length", + "translation": "നൽകിയ വാചകം പ്രതീക പരിധി കവിയുന്നു. ചാനൽ ഉദ്ദേശ്യം {{.MaxLength}} പ്രതീകങ്ങളായി പരിമിതപ്പെടുത്തിയിരിക്കുന്നു." + }, + { + "id": "api.command_channel_purpose.update_channel.app_error", + "translation": "ചാനൽ ഉദ്ദേശ്യം അപ്ഡേറ്റ് ചെയ്യുന്നതിൽ പിശക്." + }, + { + "id": "api.command_channel_purpose.permission.app_error", + "translation": "ചാനൽ ഉദ്ദേശ്യം എഡിറ്റുചെയ്യുന്നതിന് നിങ്ങൾക്ക് ഉചിതമായ അനുമതികൾ ഇല്ല." + }, + { + "id": "api.command_channel_purpose.name", + "translation": "ഉദ്ദേശ്യം" + }, + { + "id": "api.command_channel_purpose.message.app_error", + "translation": "/ഉദ്ദേശ്യ കമാൻഡ് ഉപയോഗിച്ച് ഒരു സന്ദേശം നൽകണം." + }, + { + "id": "api.command_channel_purpose.hint", + "translation": "[വാചകം]" + }, + { + "id": "api.command_channel_purpose.direct_group.app_error", + "translation": "നേരിട്ടുള്ള സന്ദേശ ചാനലുകൾക്കായി ഉദ്ദേശ്യം സജ്ജീകരിക്കാനായില്ല. പകരം തലക്കെട്ട് സജ്ജീകരിക്കാൻ /ഹെഡർ ഉപയോഗിക്കുക." + }, + { + "id": "api.command_channel_purpose.desc", + "translation": "ചാനൽ ഉദ്ദേശ്യം എഡിറ്റ് ചെയ്യുക" + }, + { + "id": "api.command_channel_purpose.channel.app_error", + "translation": "നിലവിലെ ചാനൽ വീണ്ടെടുക്കുന്നതിൽ പിശക്." + }, + { + "id": "api.command_channel_header.update_channel.max_length", + "translation": "നൽകിയ വാചകം പ്രതീക പരിധി കവിയുന്നു. ചാനൽ തലക്കെട്ട് {{.MaxLength}} പ്രതീകങ്ങളായി പരിമിതപ്പെടുത്തിയിരിക്കുന്നു." + }, + { + "id": "api.command_channel_header.update_channel.app_error", + "translation": "ചാനൽ തലക്കെട്ട് അപ്ഡേറ്റ് ചെയ്യുന്നതിൽ പിശക്." + }, + { + "id": "api.command_channel_header.permission.app_error", + "translation": "ചാനൽ തലക്കെട്ട് എഡിറ്റ് ചെയ്യാൻ നിങ്ങൾക്ക് ഉചിതമായ അനുമതികൾ ഇല്ല." + }, + { + "id": "api.command_channel_header.name", + "translation": "തലക്കെട്ട്" + }, + { + "id": "api.command_channel_header.message.app_error", + "translation": "/ഹെഡർ കമാൻഡ് ഉപയോഗിച്ച് ടെക്സ്റ്റ് നൽകണം." + }, + { + "id": "api.command_channel_header.hint", + "translation": "[വാചകം]" + }, + { + "id": "api.command.team_mismatch.app_error", + "translation": "ടീമുകളിലുടനീളം കമാൻഡുകൾ അപ്‌ഡേറ്റ് ചെയ്യാനാവുന്നില്ല." + }, + { + "id": "api.command.invite_people.sent", + "translation": "ഇമെയിൽ ക്ഷണം(കൾ) അയച്ചു" + }, + { + "id": "api.command.invite_people.no_email", + "translation": "ഒന്നോ അതിലധികമോ സാധുവായ ഇമെയിൽ വിലാസങ്ങൾ വ്യക്തമാക്കുക" + }, + { + "id": "api.command.invite_people.name", + "translation": "ആളുകളെ_ക്ഷണിക്കുക" + }, + { + "id": "api.command.invite_people.invite_off", + "translation": "ഈ സെർവറിൽ ഉപയോക്തൃ സൃഷ്‌ടി പ്രവർത്തനരഹിതമാക്കിയിരിക്കുന്നു, ക്ഷണങ്ങളൊന്നും അയച്ചിട്ടില്ല" + }, + { + "id": "api.command.invite_people.hint", + "translation": "[ പേര്@ഡൊമൈൻ.കോം …]" + }, + { + "id": "api.command.invite_people.fail", + "translation": "ഇമെയിൽ ക്ഷണം(കൾ) അയക്കുന്നതിൽ ഒരു പിശക് നേരിട്ടു" + }, + { + "id": "api.command.invite_people.email_off", + "translation": "ഇമെയിൽ കോൺഫിഗർ ചെയ്തിട്ടില്ല, ക്ഷണങ്ങളൊന്നും അയച്ചിട്ടില്ല" + }, + { + "id": "api.command.invite_people.email_invitations_off", + "translation": "ഇമെയിൽ ക്ഷണങ്ങൾ പ്രവർത്തനരഹിതമാക്കി, ക്ഷണങ്ങളൊന്നും അയച്ചിട്ടില്ല" + }, + { + "id": "api.command.invite_people.desc", + "translation": "നിങ്ങളുടെ മറ്റെർമോസ്റ് ടീമിന് ഒരു ഇമെയിൽ ക്ഷണം അയയ്ക്കുക" + }, + { + "id": "api.command.execute_command.start.app_error", + "translation": "കമാൻഡ് ട്രിഗറൊന്നും കണ്ടെത്തിയില്ല." + }, + { + "id": "api.command.execute_command.not_found.app_error", + "translation": "'{{.Trigger}}' എന്ന ട്രിഗർ ഉള്ള കമാൻഡ് കണ്ടെത്തിയില്ല. \"/\" എന്ന് തുടങ്ങുന്ന ഒരു സന്ദേശം അയക്കാൻ, സന്ദേശത്തിന്റെ തുടക്കത്തിൽ ഒരു ശൂന്യമായ ഇടം ചേർക്കാൻ ശ്രമിക്കുക." + }, + { + "id": "api.command.execute_command.format.app_error", + "translation": "കമാൻഡ് ട്രിഗർ പദത്തിൽ മുൻനിര സ്ലാഷ് പ്രതീകം നഷ്‌ടമായി" + }, + { + "id": "api.command.execute_command.failed_resp.app_error", + "translation": "'{{.Trigger}}' എന്ന ട്രിഗർ ഉള്ള കമാൻഡ് {{.Status}} എന്ന പ്രതികരണം നൽകി." + }, + { + "id": "api.command.execute_command.failed_empty.app_error", + "translation": "'{{.Trigger}}' എന്ന ട്രിഗർ ഉള്ള കമാൻഡ് ഒരു ശൂന്യമായ പ്രതികരണം നൽകി." + }, + { + "id": "api.command.execute_command.failed.app_error", + "translation": "'{{.Trigger}}' എന്ന ട്രിഗർ ഉള്ള കമാൻഡ് പരാജയപ്പെട്ടു." + }, + { + "id": "api.command.execute_command.create_post_failed.app_error", + "translation": "പ്രതികരണം പോസ്റ്റ് ചെയ്യുന്നതിൽ '{{.Trigger}}' കമാൻഡ് പരാജയപ്പെട്ടു. നിങ്ങളുടെ സിസ്റ്റം അഡ്മിനിസ്ട്രേറ്ററെ ബന്ധപ്പെടുക." + }, + { + "id": "api.command.duplicate_trigger.app_error", + "translation": "ഈ ട്രിഗർ വാക്ക് ഇതിനകം ഉപയോഗത്തിലുണ്ട്. ദയവായി മറ്റൊരു വാക്ക് തിരഞ്ഞെടുക്കുക." + }, + { + "id": "api.command.disabled.app_error", + "translation": "സിസ്റ്റം അഡ്മിൻ കമാൻഡുകൾ പ്രവർത്തനരഹിതമാക്കി." + }, + { + "id": "api.command.command_post.forbidden.app_error", + "translation": "നിർദ്ദിഷ്ട ഉപയോക്താവ് നിർദ്ദിഷ്ട ചാനലിൽ അംഗമല്ല." + }, + { + "id": "api.command.admin_only.app_error", + "translation": "സംയോജനങ്ങൾ അഡ്മിൻമാർക്ക് മാത്രമായി പരിമിതപ്പെടുത്തിയിരിക്കുന്നു." + }, + { + "id": "api.channel.update_channel.tried.app_error", + "translation": "{{.Channel}} എന്ന ഡിഫോൾട്ട് ചാനലിന്റെ ഒരു അസാധുവായ അപ്‌ഡേറ്റ് നടത്താൻ ശ്രമിച്ചു." + }, + { + "id": "api.cloud.cws_webhook_event_missing_error", + "translation": "വെബ്ഹുക്ക് ഇവന്റ് കൈകാര്യം ചെയ്തില്ല. ഒന്നുകിൽ അത് കാണുന്നില്ല അല്ലെങ്കിൽ അത് സാധുവല്ല." + }, + { + "id": "api.command_join.desc", + "translation": "തുറന്ന ചാനലിൽ ചേരുക" + }, + { + "id": "api.command_invite_people.permission.app_error", + "translation": "ഈ സെർവറിലേക്ക് പുതിയ ഉപയോക്താക്കളെ ക്ഷണിക്കാൻ നിങ്ങൾക്ക് അനുമതിയില്ല." + }, + { + "id": "api.command_invite.user_not_in_team.app_error", + "translation": "@{{.Username}} ടീമിലെ അംഗമല്ല." + }, + { + "id": "api.command_invite.user_already_in_channel.app_error", + "translation": "{{.User}} ഇതിനകം ചാനലിലുണ്ട്." + }, + { + "id": "api.command_invite.success", + "translation": "{{.ചാനൽ}} ചാനലിൽ {{.User}} ചേർത്തു." + }, + { + "id": "api.command_invite.private_channel.app_error", + "translation": "ചാനൽ {{.Channel}} കണ്ടെത്താൻ കഴിഞ്ഞില്ല. ചാനലുകൾ തിരിച്ചറിയാൻ ചാനൽ ഹാൻഡിൽ ഉപയോഗിക്കുക." + }, + { + "id": "api.command_invite.permission.app_error", + "translation": "{{.Channel}}-ൽ {{.User}}-നെ ചേർക്കാൻ നിങ്ങൾക്ക് മതിയായ അനുമതികളില്ല." + }, + { + "id": "api.command_invite.name", + "translation": "ക്ഷണിക്കുക" + }, + { + "id": "api.command_invite.missing_user.app_error", + "translation": "ഞങ്ങൾക്ക് ഉപയോക്താവിനെ കണ്ടെത്താൻ കഴിഞ്ഞില്ല. സിസ്റ്റം അഡ്മിനിസ്ട്രേറ്റർ അവ നിർജ്ജീവമാക്കിയിരിക്കാം." + }, + { + "id": "api.command_invite.missing_message.app_error", + "translation": "ഉപയോക്തൃനാമവും ചാനലും വിട്ടുപോയിരിക്കുന്നു." + }, + { + "id": "api.command_invite.group_constrained_user_denied", + "translation": "ഈ ചാനൽ നിയന്ത്രിക്കുന്നത് ഗ്രൂപ്പുകളാണ്. ഈ ചാനലുമായി സമന്വയിപ്പിച്ച ഒരു ഗ്രൂപ്പിന്റെ ഭാഗമല്ല ഈ ഉപയോക്താവ്." + }, + { + "id": "api.command_invite.fail.app_error", + "translation": "ചാനലിൽ ചേരുമ്പോൾ ഒരു തെറ്റ് സംഭവിച്ചു." + }, + { + "id": "api.command_invite.directchannel.app_error", + "translation": "നേരിട്ടുള്ള സന്ദേശ ചാനലിലേക്ക് നിങ്ങൾക്ക് ഒരാളെ ചേർക്കാനാകില്ല." + }, + { + "id": "api.command_invite.desc", + "translation": "ഒരു ചാനലിലേക്ക് ഒരു ഉപയോക്താവിനെ ക്ഷണിക്കുക" + }, + { + "id": "api.command_invite.channel.error", + "translation": "ചാനൽ {{.Channel}} കണ്ടെത്താൻ കഴിഞ്ഞില്ല. ചാനലുകൾ തിരിച്ചറിയാൻ ദയവായി [ചാനൽ ഹാൻഡിൽ](https://about.mattermost.com/default-channel-handle-documentation) ഉപയോഗിക്കുക." + }, + { + "id": "api.command_invite.channel.app_error", + "translation": "നിലവിലെ ചാനൽ വീണ്ടെടുക്കുന്നതിൽ തെറ്റ്." + }, + { + "id": "api.command_help.name", + "translation": "സഹായം" + }, + { + "id": "api.command_help.desc", + "translation": "മറ്റെർമോസ്റ് സഹായ പേജ് തുറക്കുക" + }, + { + "id": "api.command_groupmsg.permission.app_error", + "translation": "ഒരു പുതിയ ഗ്രൂപ്പ് സന്ദേശം സൃഷ്ടിക്കാൻ നിങ്ങൾക്ക് ഉചിതമായ അനുമതികൾ ഇല്ല." + }, + { + "id": "api.command_groupmsg.name", + "translation": "സന്ദേശം" + }, + { + "id": "api.command_groupmsg.min_users.app_error", + "translation": "ഗ്രൂപ്പ് സന്ദേശങ്ങൾ കുറഞ്ഞത് {{.MinUsers}} ഉപയോക്താക്കൾക്ക് മാത്രമായി പരിമിതപ്പെടുത്തിയിരിക്കുന്നു." + }, + { + "id": "api.command_groupmsg.max_users.app_error", + "translation": "ഗ്രൂപ്പ് സന്ദേശങ്ങൾ പരമാവധി {{.MaxUsers}} ഉപയോക്താക്കൾക്ക് മാത്രമായി പരിമിതപ്പെടുത്തിയിരിക്കുന്നു." + }, + { + "id": "api.command_groupmsg.invalid_user.app_error", + "translation": { + "one": "ഉപയോക്താവിനെ കണ്ടെത്താനായില്ല: {{.Users}}", + "other": "ഉപയോക്താക്കളെ കണ്ടെത്താനായില്ല: {{.Users}}" + } + }, + { + "id": "api.command_groupmsg.group_fail.app_error", + "translation": "ഗ്രൂപ്പ് സന്ദേശം സൃഷ്ടിക്കുമ്പോൾ ഒരു തെറ്റ് സംഭവിച്ചു." + }, + { + "id": "api.command_groupmsg.fail.app_error", + "translation": "ഉപയോക്താക്കൾക്ക് സന്ദേശമയയ്‌ക്കുമ്പോൾ ഒരു തെറ്റ് സംഭവിച്ചു." + }, + { + "id": "api.command_groupmsg.desc", + "translation": "നിർദ്ദിഷ്ട ഉപയോക്താക്കൾക്ക് ഒരു ഗ്രൂപ്പ് സന്ദേശം അയയ്ക്കുന്നു" + }, + { + "id": "api.command_expand_collapse.fail.app_error", + "translation": "പ്രിവ്യൂ വിപുലീകരിക്കുമ്പോൾ ഒരു തെറ്റ് സംഭവിച്ചു." + }, + { + "id": "api.command_expand.success", + "translation": "ഇമേജ് ലിങ്കുകൾ ഇപ്പോൾ ഡിഫോൾട്ടായി വികസിക്കുന്നു" + }, + { + "id": "api.command_expand.name", + "translation": "വികസിപ്പിക്കുക" + }, + { + "id": "api.command_expand.desc", + "translation": "ഇമേജ് പ്രിവ്യൂകളുടെ സ്വയമേവ തകരുന്നത് ഓഫാക്കുക" + }, + { + "id": "api.command_echo.name", + "translation": "എക്കോ" + }, + { + "id": "api.command_echo.message.app_error", + "translation": "/echo കമാൻഡ് ഉപയോഗിച്ച് ഒരു സന്ദേശം നൽകണം." + }, + { + "id": "api.command_echo.hint", + "translation": "'സന്ദേശം' [സെക്കൻഡിനുള്ളിൽ കാലതാമസം]" + }, + { + "id": "api.command_echo.high_volume.app_error", + "translation": "ഉയർന്ന അളവിലുള്ള എക്കോ അഭ്യർത്ഥന, അഭ്യർത്ഥന പ്രോസസ്സ് ചെയ്യാൻ കഴിയില്ല." + }, + { + "id": "api.command_echo.desc", + "translation": "നിങ്ങളുടെ അക്കൗണ്ടിൽ നിന്ന് എക്കോ ബാക്ക് ടെക്സ്റ്റ്" + }, + { + "id": "api.command_echo.delay.app_error", + "translation": "കാലതാമസം 10000 സെക്കൻഡിൽ താഴെയായിരിക്കണം." + }, + { + "id": "api.command_dnd.success", + "translation": "ശല്യപ്പെടുത്തരുത് പ്രവർത്തനക്ഷമമാക്കി. ശല്യപ്പെടുത്തരുത് ഓഫാക്കുന്നതുവരെ നിങ്ങൾക്ക് ഡെസ്ക്ടോപ്പ് അല്ലെങ്കിൽ മൊബൈൽ പുഷ് അറിയിപ്പുകൾ ലഭിക്കില്ല." + }, + { + "id": "api.command_dnd.desc", + "translation": "ശല്യപ്പെടുത്തരുത് ഡെസ്ക്ടോപ്പ്, മൊബൈൽ പുഷ് അറിയിപ്പുകൾ പ്രവർത്തനരഹിതമാക്കുന്നു." + }, + { + "id": "api.command_collapse.success", + "translation": "ഇമേജ് ലിങ്കുകൾ ഇപ്പോൾ ഡിഫോൾട്ടായി തകരുന്നു" + }, + { + "id": "api.command_collapse.name", + "translation": "തകർച്ച" + }, + { + "id": "api.command_collapse.desc", + "translation": "ഇമേജ് പ്രിവ്യൂകളുടെ സ്വയമേവ തകരുന്നത് ഓണാക്കുക" + }, + { + "id": "api.command_code.name", + "translation": "കോഡ്" + }, + { + "id": "api.command_code.message.app_error", + "translation": "/കോഡ് കമാൻഡ് ഉപയോഗിച്ച് ഒരു സന്ദേശം നൽകണം." + }, + { + "id": "api.command_code.hint", + "translation": "[ടെക്സ്റ്റ്]" + }, + { + "id": "api.command_code.desc", + "translation": "ഒരു കോഡ് ബ്ലോക്കായി ടെക്സ്റ്റ് പ്രദർശിപ്പിക്കുക" + }, + { + "id": "api.command_channel_rename.update_channel.app_error", + "translation": "നിലവിലെ ചാനൽ അപ്ഡേറ്റ് ചെയ്യുന്നതിൽ തെറ്റ്." + }, + { + "id": "api.command_channel_rename.too_short.app_error", + "translation": "ചാനലിന്റെ പേര് {{.Length}} അല്ലെങ്കിൽ കൂടുതൽ പ്രതീകങ്ങൾ ആയിരിക്കണം." + }, + { + "id": "api.command_channel_rename.too_long.app_error", + "translation": "ചാനലിന്റെ പേര് {{.Length}} അല്ലെങ്കിൽ കുറച്ച് പ്രതീകങ്ങൾ ആയിരിക്കണം." + }, + { + "id": "api.cloud.request_error", + "translation": "CWS-ലേക്കുള്ള അഭ്യർത്ഥന പ്രോസസ്സ് ചെയ്യുന്നതിൽ തെറ്റ്." + }, + { + "id": "api.command_remove.hint", + "translation": "@[ഉപയോക്തൃനാമം]" + }, + { + "id": "api.command_remove.group_constrained_user_denied", + "translation": "ഈ ചാനലുമായി ലിങ്ക് ചെയ്‌തിരിക്കുന്ന ഗ്രൂപ്പുകളിൽ അംഗമായതിനാൽ ഉപയോക്താവിനെ നിങ്ങൾക്ക് ചാനലിൽ നിന്ന് നീക്കം ചെയ്യാൻ കഴിയില്ല. ഈ ചാനലിൽ നിന്ന് അവരെ നീക്കം ചെയ്യാൻ, ലിങ്ക് ചെയ്ത ഗ്രൂപ്പുകളിൽ നിന്ന് അവരെ നീക്കം ചെയ്യണം." + }, + { + "id": "api.command_remove.direct_group.app_error", + "translation": "നേരിട്ടുള്ള സന്ദേശ ചാനലിൽ നിന്ന് നിങ്ങൾക്ക് ഒരാളെ നീക്കം ചെയ്യാൻ കഴിയില്ല." + }, + { + "id": "api.command_remove.desc", + "translation": "ചാനലിൽ നിന്ന് ഒരു അംഗത്തെ നീക്കം ചെയ്യുക" + }, + { + "id": "api.command_remote.remote_add_remove.help", + "translation": "സുരക്ഷിത കണക്ഷനുകൾ ചേർക്കുക/നീക്കം ചെയ്യുക. ലഭ്യമായ പ്രവർത്തനങ്ങൾ: {{.Actions}}" + }, + { + "id": "api.command_remote.invite.help", + "translation": "ഒരു സുരക്ഷിത കണക്ഷൻ ക്ഷണിക്കുക" + }, + { + "id": "api.command_remote.invitation_created", + "translation": "ക്ഷണം സൃഷ്ടിച്ചു." + }, + { + "id": "api.command_remote.fetch_status.error", + "translation": "സുരക്ഷിത കണക്ഷനുകൾ ലഭ്യമാക്കാൻ കഴിഞ്ഞില്ല: {{.Error}}" + }, + { + "id": "api.command_remote.encrypt_invitation.error", + "translation": "ക്ഷണം എൻക്രിപ്റ്റ് ചെയ്യാൻ കഴിഞ്ഞില്ല: {{.Error}}" + }, + { + "id": "api.command_remote.decode_invitation.error", + "translation": "ക്ഷണം ഡീകോഡ് ചെയ്യാൻ കഴിഞ്ഞില്ല: {{.Error}}" + }, + { + "id": "api.command_remote.cluster_removed", + "translation": "സുരക്ഷിത കണക്ഷൻ {{.RemoteId}} {{.Result}}." + }, + { + "id": "api.command_remote.add_remote.error", + "translation": "സുരക്ഷിത കണക്ഷൻ ചേർക്കാൻ കഴിഞ്ഞില്ല: {{.Error}}" + }, + { + "id": "api.command_remote.accept_invitation.error", + "translation": "ക്ഷണം സ്വീകരിക്കാൻ കഴിഞ്ഞില്ല: {{.Error}}" + }, + { + "id": "api.command_mute.name", + "translation": "നിശബ്ദമാക്കുക" + }, + { + "id": "api.command_offline.desc", + "translation": "നിങ്ങളുടെ സ്റ്റാറ്റസ് ഓഫ്‌ലൈനായി സജ്ജമാക്കുക" + }, + { + "id": "api.command_open.name", + "translation": "തുറക്കുക" + }, + { + "id": "api.command_online.success", + "translation": "നിങ്ങൾ ഇപ്പോൾ ഓൺലൈനിലാണ്" + }, + { + "id": "api.command_online.name", + "translation": "ഓൺലൈൻ" + }, + { + "id": "api.command_online.desc", + "translation": "നിങ്ങളുടെ സ്റ്റാറ്റസ് ഓൺലൈനിൽ സജ്ജമാക്കുക" + }, + { + "id": "api.command_offline.success", + "translation": "നിങ്ങൾ ഇപ്പോൾ ഓഫ്‌ലൈനാണ്" + }, + { + "id": "api.command_offline.name", + "translation": "ഓഫ്‌ലൈൻ" + }, + { + "id": "api.command_mute.success_unmute_direct_msg", + "translation": "ഈ ചാനൽ ഇനി നിശബ്ദമാക്കിയിട്ടില്ല." + }, + { + "id": "api.command_mute.success_unmute", + "translation": "{{.Channel}} ഇപ്പോൾ നിശബ്ദമാക്കിയിട്ടില്ല." + }, + { + "id": "api.command_mute.success_mute_direct_msg", + "translation": "ചാനൽ നിശബ്‌ദമാക്കുന്നത് ഓഫാക്കുന്നതുവരെ ഈ ചാനലിനായി നിങ്ങൾക്ക് അറിയിപ്പുകൾ ലഭിക്കില്ല." + }, + { + "id": "api.command_mute.success_mute", + "translation": "ചാനൽ നിശബ്ദമാക്കുന്നത് വരെ നിങ്ങൾക്ക് {{.Channel}}-നായി അറിയിപ്പുകൾ ലഭിക്കില്ല." + }, + { + "id": "api.command_mute.not_member.error", + "translation": "നിങ്ങൾ അംഗമല്ലാത്തതിനാൽ {{.Channel}} ചാനൽ നിശബ്ദമാക്കാൻ കഴിഞ്ഞില്ല." + }, + { + "id": "api.command_mute.no_channel.error", + "translation": "നിർദ്ദിഷ്ട ചാനൽ കണ്ടെത്താൻ കഴിഞ്ഞില്ല. ചാനലുകൾ തിരിച്ചറിയാൻ ദയവായി [ചാനൽ ഹാൻഡിൽ](https://about.mattermost.com/default-channel-handle-documentation) ഉപയോഗിക്കുക." + }, + { + "id": "api.command_msg.hint", + "translation": "@[ഉപയോക്തൃനാമം] 'സന്ദേശം'" + }, + { + "id": "api.command_msg.missing.app_error", + "translation": "ഉപയോക്താവിനെ കണ്ടെത്താനായില്ല." + }, + { + "id": "api.command_me.desc", + "translation": "ഒരു പ്രവർത്തനം നടത്തുക" + }, + { + "id": "api.command_join.hint", + "translation": "~[ചാനൽ]" + }, + { + "id": "api.command_me.hint", + "translation": "[സന്ദേശം]" + }, + { + "id": "api.command_mute.hint", + "translation": "~[ചാനൽ]" + }, + { + "id": "api.command_mute.error", + "translation": "ചാനൽ {{.Channel}} കണ്ടെത്താൻ കഴിഞ്ഞില്ല. ചാനലുകൾ തിരിച്ചറിയാൻ ദയവായി [ചാനൽ ഹാൻഡിൽ](https://about.mattermost.com/default-channel-handle-documentation) ഉപയോഗിക്കുക." + }, + { + "id": "api.command_mute.desc", + "translation": "ഡെസ്‌ക്‌ടോപ്പ്, ഇമെയിൽ, പുഷ് അറിയിപ്പുകൾ എന്നിവ നിലവിലെ ചാനലിന് അല്ലെങ്കിൽ വ്യക്തമാക്കിയ [ചാനൽ] ഓഫാക്കുന്നു." + }, + { + "id": "api.command_msg.permission.app_error", + "translation": "ഈ ഉപയോക്താവിന് നേരിട്ട് സന്ദേശം അയയ്‌ക്കാൻ നിങ്ങൾക്ക് ഉചിതമായ അനുമതികൾ ഇല്ല." + }, + { + "id": "api.command_msg.name", + "translation": "സന്ദേശം" + }, + { + "id": "api.command_msg.fail.app_error", + "translation": "ഉപയോക്താവിന് സന്ദേശമയയ്‌ക്കുമ്പോൾ ഒരു തെറ്റ് സംഭവിച്ചു." + }, + { + "id": "api.command_msg.dm_fail.app_error", + "translation": "നേരിട്ടുള്ള സന്ദേശം സൃഷ്ടിക്കുമ്പോൾ ഒരു തെറ്റ് സംഭവിച്ചു." + }, + { + "id": "api.command_msg.desc", + "translation": "ഒരു ഉപയോക്താവിന് നേരിട്ടുള്ള സന്ദേശം അയയ്‌ക്കുക" + }, + { + "id": "api.command_me.name", + "translation": "എന്നെ" + }, + { + "id": "api.command_logout.name", + "translation": "ലോഗൗട്ട്" + }, + { + "id": "api.command_logout.desc", + "translation": "മാറ്റർമോസ്റ്റിന്റെ ലോഗ്ഔട്ട്" + }, + { + "id": "api.command_leave.fail.app_error", + "translation": "ചാനൽ വിടുമ്പോൾ ഒരു തെറ്റ് സംഭവിച്ചു." + }, + { + "id": "api.command_leave.desc", + "translation": "നിലവിലെ ചാനൽ വിടുക" + }, + { + "id": "api.command_join.name", + "translation": "ചേരുക" + }, + { + "id": "api.command_join.missing.app_error", + "translation": "ചാനൽ കണ്ടെത്താനായില്ല." + }, + { + "id": "api.command_join.list.app_error", + "translation": "ചാനലുകൾ ലിസ്റ്റുചെയ്യുമ്പോൾ ഒരു തെറ്റ് സംഭവിച്ചു." + }, + { + "id": "api.command_join.fail.app_error", + "translation": "ചാനലിൽ ചേരുമ്പോൾ ഒരു തെറ്റ് സംഭവിച്ചു." + }, + { + "id": "api.command_custom_status.name", + "translation": "സ്റ്റാറ്റസ്" + }, + { + "id": "api.command_custom_status.success", + "translation": "നിങ്ങളുടെ സ്റ്റാറ്റസ് “{{.EmojiName}} {{.StatusMessage}}” ആയി സജ്ജീകരിച്ചിരിക്കുന്നു. ചാനൽ സൈഡ്‌ബാർ ഹെഡറിലെ സ്റ്റാറ്റസ് പോപ്പോവറിൽ നിന്ന് നിങ്ങൾക്ക് നിങ്ങളുടെ സ്റ്റാറ്റസ് മാറ്റാനാകും." + }, + { + "id": "api.command_custom_status.desc", + "translation": "നിങ്ങളുടെ സ്റ്റാറ്റസ് സജ്ജമാക്കുക അല്ലെങ്കിൽ മായ്‌ക്കുക" + }, + { + "id": "api.command_custom_status.clear.success", + "translation": "നിങ്ങളുടെ നില മായ്ച്ചു." + }, + { + "id": "api.command_custom_status.clear.app_error", + "translation": "സ്റ്റാറ്റസ് മായ്‌ക്കുന്നതിൽ തെറ്റ്." + }, + { + "id": "api.command_custom_status.app_error", + "translation": "സ്റ്റാറ്റസ് സജ്ജീകരിക്കുന്നതിൽ തെറ്റ്." + }, + { + "id": "api.command_share.permission_required", + "translation": "പങ്കിട്ട ചാനലുകൾ നിയന്ത്രിക്കാൻ നിങ്ങൾക്ക് `{{.Permission}}` അനുമതി ആവശ്യമാണ്." + }, + { + "id": "api.command_share.channel_status.help", + "translation": "ഈ പങ്കിട്ട ചാനലിന്റെ സ്റ്റാറ്റസ് പ്രദർശിപ്പിക്കുന്നു" + }, + { + "id": "api.command_share.channel_shared", + "translation": "ഈ ചാനൽ ഇപ്പോൾ പങ്കിട്ടു." + }, + { + "id": "api.command_share.channel_remote_id_not_exists", + "translation": "പങ്കിട്ട ചാനൽ സുരക്ഷിത കണക്ഷൻ `{{.RemoteId}}` ഈ ചാനലിനായി നിലവിലില്ല." + }, + { + "id": "api.command_share.channel_invite_not_home.error", + "translation": "മറ്റെവിടെയെങ്കിലും ഉത്ഭവിക്കുന്ന പങ്കിട്ട ചാനലിലേക്ക് സുരക്ഷിത കണക്ഷൻ ക്ഷണിക്കാനാവില്ല." + }, + { + "id": "api.command_search.name", + "translation": "തിരയുക" + }, + { + "id": "api.command_settings.unsupported.app_error", + "translation": "settings കമാൻഡ് നിങ്ങളുടെ ഉപകരണത്തിൽ പിന്തുണയ്ക്കുന്നില്ല." + }, + { + "id": "api.command_settings.name", + "translation": "ക്രമീകരണങ്ങൾ" + }, + { + "id": "api.command_search.unsupported.app_error", + "translation": "നിങ്ങളുടെ ഉപകരണത്തിൽ search കമാൻഡ് പിന്തുണയ്ക്കുന്നില്ല." + }, + { + "id": "api.command_search.desc", + "translation": "സന്ദേശങ്ങളിൽ ടെക്സ്റ്റ് തിരയുക" + }, + { + "id": "api.command_remove.user_not_in_channel", + "translation": "{{.Username}} ഈ ചാനലിൽ അംഗമല്ല." + }, + { + "id": "api.command_remove.permission.app_error", + "translation": "അംഗത്തെ നീക്കം ചെയ്യുന്നതിനുള്ള ഉചിതമായ അനുമതികൾ നിങ്ങൾക്കില്ല." + }, + { + "id": "api.command_remove.name", + "translation": "നീക്കം ചെയ്യുക" + }, + { + "id": "api.command_remove.missing.app_error", + "translation": "ഞങ്ങൾക്ക് ഉപയോക്താവിനെ കണ്ടെത്താൻ കഴിഞ്ഞില്ല. സിസ്റ്റം അഡ്മിനിസ്ട്രേറ്റർ അവ നിർജ്ജീവമാക്കിയിരിക്കാം." + }, + { + "id": "api.command_remove.message.app_error", + "translation": "/remove അല്ലെങ്കിൽ /kick കമാൻഡ് ഉപയോഗിച്ച് ഒരു സന്ദേശം നൽകണം." + } +] diff --git a/server/i18n/mn.json b/server/i18n/mn.json new file mode 100644 index 00000000000..b86508fefc2 --- /dev/null +++ b/server/i18n/mn.json @@ -0,0 +1,86 @@ +[ + { + "id": "api.team.get_all_teams.insufficient_permissions", + "translation": "Уучлаарай, танд группын товч тайлбар/уриаг өөрчлөх эрх байхгүй байна" + }, + { + "id": "api.command_remove.permission.app_error", + "translation": "Уучлаарай, танд группын товч тайлбар/уриаг өөрчлөх эрх байхгүй байна" + }, + { + "id": "api.command_msg.permission.app_error", + "translation": "Уучлаарай, танд группын товч тайлбар/уриаг өөрчлөх эрх байхгүй байна" + }, + { + "id": "api.command_channel_rename.permission.app_error", + "translation": "Уучлаарай, танд группын товч тайлбар/уриаг өөрчлөх эрх байхгүй байна" + }, + { + "id": "api.command_channel_purpose.permission.app_error", + "translation": "Уучлаарай, танд группын товч тайлбар/уриаг өөрчлөх эрх байхгүй байна" + }, + { + "id": "api.command_channel_purpose.desc", + "translation": "Группыг зохистой ашиглах тухай мэдээлэл өөрчлөх" + }, + { + "id": "api.command_channel_header.permission.app_error", + "translation": "Уучлаарай, танд группын товч тайлбар/уриаг өөрчлөх эрх байхгүй байна" + }, + { + "id": "api.command_channel_header.desc", + "translation": "Группын товч тайлбар/уриаг өөрчлөх" + }, + { + "id": "api.command.invite_people.name", + "translation": "invite_people" + }, + { + "id": "September", + "translation": "9-р сар" + }, + { + "id": "October", + "translation": "10-р сар" + }, + { + "id": "November", + "translation": "11-р сар" + }, + { + "id": "May", + "translation": "5-р сар" + }, + { + "id": "March", + "translation": "3-р сар" + }, + { + "id": "June", + "translation": "6-р сар" + }, + { + "id": "July", + "translation": "7-р сар" + }, + { + "id": "January", + "translation": "1-р сар" + }, + { + "id": "February", + "translation": "2-р сар" + }, + { + "id": "December", + "translation": "12-р сар" + }, + { + "id": "August", + "translation": "8-р сар" + }, + { + "id": "April", + "translation": "4-р сар" + } +] diff --git a/server/i18n/nb_NO.json b/server/i18n/nb_NO.json new file mode 100644 index 00000000000..dc22d0fa1de --- /dev/null +++ b/server/i18n/nb_NO.json @@ -0,0 +1,86 @@ +[ + { + "id": "api.team.get_all_teams.insufficient_permissions", + "translation": "Уучлаарай, танд группын товч тайлбар/уриаг өөрчлөх эрх байхгүй байна" + }, + { + "id": "api.command_remove.permission.app_error", + "translation": "Уучлаарай, танд группын товч тайлбар/уриаг өөрчлөх эрх байхгүй байна" + }, + { + "id": "api.command_msg.permission.app_error", + "translation": "Уучлаарай, танд группын товч тайлбар/уриаг өөрчлөх эрх байхгүй байна" + }, + { + "id": "api.command_channel_rename.permission.app_error", + "translation": "Уучлаарай, танд группын товч тайлбар/уриаг өөрчлөх эрх байхгүй байна" + }, + { + "id": "api.command_channel_purpose.permission.app_error", + "translation": "Уучлаарай, танд группын товч тайлбар/уриаг өөрчлөх эрх байхгүй байна" + }, + { + "id": "api.command_channel_purpose.desc", + "translation": "Группыг зохистой ашиглах тухай мэдээлэл өөрчлөх" + }, + { + "id": "api.command_channel_header.permission.app_error", + "translation": "Уучлаарай, танд группын товч тайлбар/уриаг өөрчлөх эрх байхгүй байна" + }, + { + "id": "api.command_channel_header.desc", + "translation": "Группын товч тайлбар/уриаг өөрчлөх" + }, + { + "id": "api.command.invite_people.name", + "translation": "invite_people" + }, + { + "id": "September", + "translation": "September" + }, + { + "id": "October", + "translation": "Oktober" + }, + { + "id": "November", + "translation": "November" + }, + { + "id": "May", + "translation": "Mai" + }, + { + "id": "March", + "translation": "Mars" + }, + { + "id": "June", + "translation": "Juni" + }, + { + "id": "July", + "translation": "Juli" + }, + { + "id": "January", + "translation": "Januar" + }, + { + "id": "February", + "translation": "Februar" + }, + { + "id": "December", + "translation": "Desember" + }, + { + "id": "August", + "translation": "August" + }, + { + "id": "April", + "translation": "April" + } +] diff --git a/server/i18n/ne.json b/server/i18n/ne.json new file mode 100644 index 00000000000..e25b26a6aeb --- /dev/null +++ b/server/i18n/ne.json @@ -0,0 +1,38 @@ +[ + { + "id": "system.message.name", + "translation": "प्रणाली" + }, + { + "id": "app.import.validate_direct_post_import_data.message_length.error", + "translation": "सन्देश धेरै लामो छ" + }, + { + "id": "web.error.unsupported_browser.system_browser_or", + "translation": "वा" + }, + { + "id": "api.admin.add_certificate.saving.app_error", + "translation": "प्रमाणपत्र राख्न असमर्थ ।" + }, + { + "id": "api.admin.add_certificate.open.app_error", + "translation": "प्रमाणपत्र खोल्न असमर्थ ।" + }, + { + "id": "api.admin.add_certificate.array.app_error", + "translation": "'प्रमाणपत्र' अन्तर्गत अनुरोधमा कुनै फाइल छैन।" + }, + { + "id": "Playbooks", + "translation": "प्लेबुकहरू" + }, + { + "id": "Channels", + "translation": "च्यानलहरू" + }, + { + "id": "Boards", + "translation": "बोर्डहरू" + } +] diff --git a/server/i18n/nl.json b/server/i18n/nl.json index e19a5ac8a8e..7aecd2f6651 100644 --- a/server/i18n/nl.json +++ b/server/i18n/nl.json @@ -4039,10 +4039,6 @@ "id": "model.emoji.user_id.app_error", "translation": "Ongeldig creator id." }, - { - "id": "model.file_info.get.gif.app_error", - "translation": "Kan gif niet decoderen." - }, { "id": "model.file_info.is_valid.create_at.app_error", "translation": "Ongeldige waarde voor create_at." @@ -4777,7 +4773,7 @@ }, { "id": "web.error.unsupported_browser.min_browser_version.edge", - "translation": "Versie 110+" + "translation": "Versie 112+" }, { "id": "web.error.unsupported_browser.min_browser_version.chrome", @@ -9102,10 +9098,6 @@ "id": "api.license.request_trial_license.embargoed", "translation": "We konden het verzoek niet verwerken vanwege beperkingen voor landen die onder een embargo vallen. [Meer informatie vind je in onze documentatie](https://mattermost.com/pl/limitations-for-embargoed-countries), of neem contact op met legal@mattermost.com voor vragen over exportbeperkingen." }, - { - "id": "api.insights.feature_disabled", - "translation": "Insights zit achter een functionaliteit die niet is ingeschakeld." - }, { "id": "api.getThreadsForUser.bad_only_params", "translation": "De parameters OnlyThreads en OnlyTotals voor getThreadsForUser sluiten elkaar uit" @@ -9122,10 +9114,6 @@ "id": "api.templates.cloud_upgrade_confirmation.subject", "translation": "Bevestiging Mattermost upgrade" }, - { - "id": "app.recent_searches.app_error", - "translation": "Fout bij ophalen van de recente zoekopdrachten" - }, { "id": "api.file.cloud_upload.app_error", "translation": "Uploaden via mmctl naar een Cloud instance wordt niet ondersteund. Raadpleeg de documentatie hier: https://docs.mattermost.com/manage/cloud-data-export.html." @@ -9154,18 +9142,6 @@ "id": "api.cloud.teams_limit_reached.create", "translation": "Kan geen team aanmaken omdat de teamlimiet bereikt is" }, - { - "id": "app.post.get_top_threads_for_user_since.app_error", - "translation": "Kan de topdraadjes voor de gebruiker niet ophalen." - }, - { - "id": "app.post.get_top_threads_for_team_since.app_error", - "translation": "Kan de topdraadjes voor het team niet ophalen." - }, - { - "id": "app.insights.feature_disabled", - "translation": "Insights-functionaliteit is uitgeschakeld." - }, { "id": "common.parse_error_int64", "translation": "Mislukt om de waarde te ontleden:{{.Waarde}} naar int64" @@ -9198,10 +9174,6 @@ "id": "app.team.clear_cache.app_error", "translation": "Fout bij het wissen van de cache van teamleden" }, - { - "id": "app.post.get_top_dms_for_user_since.app_error", - "translation": "Fout bij het ophalen van de top-privé-berichten voor gebruiker." - }, { "id": "api.templates.delinquency_14.subject", "translation": "Betaling is laattijdig voor jouw Mattermost {{.Plan}}" @@ -9466,14 +9438,6 @@ "id": "api.team.invite_guests_to_channels.disabled.error", "translation": "Gastaccounts zijn uitgeschakeld" }, - { - "id": "app.collection.add_topic.exists.app_error", - "translation": "Topictype bestaat al." - }, - { - "id": "app.collection.add_collection.exists.app_error", - "translation": "Collectietype bestaat al." - }, { "id": "api.user.add_user_to_group_syncables.not_ldap_user.app_error", "translation": "geen ldap-gebruiker" @@ -9494,10 +9458,6 @@ "id": "api.acknowledgement.delete.archived_channel.app_error", "translation": "Je kan een bevestiging in een gearchiveerd kanaal niet verwijderen." }, - { - "id": "worktemplate.category.product_teams", - "translation": "Productteams" - }, { "id": "model.draft.is_valid.user_id.app_error", "translation": "Ongeldig gebruikers-id." @@ -9542,14 +9502,6 @@ "id": "model.acknowledgement.is_valid.post_id.app_error", "translation": "Ongeldig bericht-id." }, - { - "id": "app.worktemplates.get_templates.app_error", - "translation": "Kon geen werksjablonen ophalen" - }, - { - "id": "app.worktemplates.get_categories.app_error", - "translation": "Kan geen werksjablooncategorieën ophalen" - }, { "id": "app.post_prority.get_for_post.app_error", "translation": "Kon geen berichtprioriteit ophalen voor bericht" @@ -9694,66 +9646,6 @@ "id": "api.config.update_config.translations.app_error", "translation": "Fout bij het bijwerken van de serververtalingen." }, - { - "id": "worktemplate.category.leadership", - "translation": "Leiderschap" - }, - { - "id": "worktemplate.category.devops", - "translation": "DevOps" - }, - { - "id": "worktemplate.category.companywide", - "translation": "Bedrijfswijd" - }, - { - "id": "app.worktemplates.execute_work_template.playbooks.find_channel_error", - "translation": "Kan kanaal verbonden met een playbook niet vinden." - }, - { - "id": "app.worktemplates.execute_work_template.playbooks.create_error", - "translation": "Fout bij het aanmaken van een playbook." - }, - { - "id": "app.worktemplates.execute_work_template.channels.create_error", - "translation": "Fout bij het maken van een kanaal." - }, - { - "id": "app.worktemplates.execute_work_template.boards.create_error", - "translation": "Fout bij het aanmaken van een bord." - }, - { - "id": "app.worktemplates.execute_work_template.app_error", - "translation": "Fout bij het uitvoeren van een werksjabloon." - }, - { - "id": "app.worktemplate.execution_request.license_cannot_create_private_playbook", - "translation": "Jouw licentie ondersteunt geen privé-playbooks." - }, - { - "id": "app.worktemplate.execution_request.cannot_create_public_playbook", - "translation": "Je hebt geen rechten om een publiek playbook te maken." - }, - { - "id": "app.worktemplate.execution_request.cannot_create_public_channel", - "translation": "Je hebt geen rechten om een publiek kanaal te maken." - }, - { - "id": "app.worktemplate.execution_request.cannot_create_public_board", - "translation": "Je hebt geen rechten om een openbaar bord aan te maken." - }, - { - "id": "app.worktemplate.execution_request.cannot_create_private_playbook", - "translation": "Je hebt geen rechten om een privé playbook aan te maken." - }, - { - "id": "app.worktemplate.execution_request.cannot_create_private_channel", - "translation": "Je hebt geen rechten om een privékanaal te maken." - }, - { - "id": "app.worktemplate.execution_request.cannot_create_private_board", - "translation": "Je hebt geen rechten om een privé-bord te maken." - }, { "id": "app.import.validate_user_teams_import_data.invalid_auth_service.error", "translation": "Ongeldige auth service: {{.AuthService}}" @@ -9762,50 +9654,6 @@ "id": "app.group.username_conflict", "translation": "Er bestaat al een gebruiker met gebruikersnaam \"{{.Gebruikersnaam}}\"." }, - { - "id": "api.work_templates.disabled", - "translation": "Werksjablonen zijn uitgeschakeld." - }, - { - "id": "api.command_templates.unsupported.app_error", - "translation": "De sjablonenopdracht wordt niet ondersteund op jouw toestel." - }, - { - "id": "api.command_templates.name", - "translation": "sjablonen" - }, - { - "id": "api.command_templates.desc", - "translation": "Open het venster Aanmaken van sjabloon" - }, - { - "id": "app.worktemplates.execute_work_template.name_too_long", - "translation": "Het naamveld kan niet meer dan 64 tekens bevatten." - }, - { - "id": "app.team.create_onboarding_linked_board.product_key_not_found", - "translation": "productsleutel niet gevonden" - }, - { - "id": "app.team.create_onboarding_linked_board.itf_not_of_type", - "translation": "interface niet van het verwachte type" - }, - { - "id": "app.team.create_onboarding_linked_board.error_patching_board", - "translation": "fout tijdens het patchen van het bord" - }, - { - "id": "app.team.create_onboarding_linked_board.error_no_board", - "translation": "fout geen bord aangemaakt" - }, - { - "id": "app.team.create_onboarding_linked_board.error_getting_templates", - "translation": "fout bij het ophalen van de bordsjablonen" - }, - { - "id": "app.team.create_onboarding_linked_board.error_duplicating_board", - "translation": "fout bij het dupliceren van het bord" - }, { "id": "api.system.logs.invalidFilter", "translation": "Ongeldig logboekfilter" @@ -9822,34 +9670,6 @@ "id": "api.templates.license_up_for_renewal_contact_sales", "translation": "Contacteer de verkoopsafdeling" }, - { - "id": "app.user.digest.tasks.due_today", - "translation": "Vandaag te voldoen" - }, - { - "id": "app.user.digest.tasks.all_tasks_command", - "translation": "Gebruik `/playbook todo` om al je taken te zien." - }, - { - "id": "app.user.digest.runs_in_progress.zero_in_progress", - "translation": "Je hebt momenteel 0 runs lopen." - }, - { - "id": "app.user.digest.runs_in_progress.heading", - "translation": "Runs in uitvoering" - }, - { - "id": "app.user.digest.overdue_status_updates.zero_overdue", - "translation": "Je hebt 0 runs achterstand." - }, - { - "id": "app.user.digest.overdue_status_updates.heading", - "translation": "Achterstallige statusupdates" - }, - { - "id": "app.command.execute.error", - "translation": "Kan commando niet uitvoeren." - }, { "id": "api.server.cws.subscribe_to_newsletter.app_error", "translation": "CWS-server kan zich niet abonneren op nieuwsbrief." @@ -9862,184 +9682,212 @@ "id": "api.license.request-trial.bad-request.business-email", "translation": "Ongeldig zakelijk e-mailadres voor proefperiode" }, - { - "id": "app.user.run.add_to_timeline.playbook_run", - "translation": "Playbook Uitvoering" - }, - { - "id": "app.user.run.add_checklist_item.title", - "translation": "Nieuwe taak toevoegen" - }, - { - "id": "app.user.run.add_checklist_item.submit_label", - "translation": "Taak toevoegen" - }, - { - "id": "app.user.run.add_checklist_item.name", - "translation": "Naam" - }, - { - "id": "app.user.run.add_checklist_item.description", - "translation": "Omschrijving" - }, - { - "id": "app.user.new_run.title", - "translation": "Playbook uitvoeren" - }, - { - "id": "app.user.new_run.submit_label", - "translation": "Uitvoering starten" - }, - { - "id": "app.user.new_run.run_name", - "translation": "Naam van de uitvoering" - }, - { - "id": "app.user.new_run.playbook", - "translation": "Playbook" - }, - { - "id": "app.user.new_run.intro", - "translation": "**Eigenaar** {{.Username}}" - }, - { - "id": "app.user.digest.tasks.zero_assigned", - "translation": "Je hebt 0 toegewezen taken." - }, - { - "id": "app.user.digest.tasks.num_assigned_due_until_today", - "translation": { - "one": "Je hebt {{.Count}} taak toegewezen gekregen die nu verloopt:", - "other": "Je hebt {{.Count}} taken toegewezen gekregen die nu verlopen:" - } - }, - { - "id": "app.user.digest.tasks.num_assigned", - "translation": { - "one": "Je hebt {{.Count}} toegewezen taak:", - "other": "Je hebt {{.Count}} toegewezen taken:" - } - }, - { - "id": "app.user.digest.tasks.heading", - "translation": "Jouw toegewezen taken" - }, - { - "id": "app.user.digest.tasks.due_yesterday", - "translation": "Verliep gisteren" - }, - { - "id": "app.user.digest.tasks.due_x_days_ago", - "translation": "Verliep {{.Count}} dagen geleden" - }, - { - "id": "app.user.digest.tasks.due_in_x_days", - "translation": { - "one": "Verloopt binnen {{.Count}} dag", - "other": "Verloopt binnen {{.Count}} dagen" - } - }, - { - "id": "app.user.digest.tasks.due_after_today", - "translation": { - "one": "Je hebt **{.Count}} taak toegewezen gekregen die verloopt na vandaag**.", - "other": "Je hebt **{.Count}} taken toegewezen gekregen die verlopen na vandaag**." - } - }, - { - "id": "app.user.digest.runs_in_progress.num_in_progress", - "translation": { - "one": "Je hebt {{.Count}} uitvoering lopen:", - "other": "Je hebt {{.Count}} uitvoeringen lopen:" - } - }, - { - "id": "app.user.digest.overdue_status_updates.num_overdue", - "translation": { - "one": "Je hebt {{.Count}} uitvoering die een statusupdate nodig hebben:", - "other": "Je hebt {{.Count}} uitvoeringen die een statusupdate nodig hebben:" - } - }, { "id": "app.plugin.skip_installation.app_error", "translation": "De installatie van plugin {{.Id}} wordt overgeslagen omdat de bestaande versie gelijk is of nieuwer." }, { - "id": "app.plugin.blocked.app_error", - "translation": "Plugin {{.Id}} staat op de lijst met geblokkeerde apps. Sommige plugins worden geblokkeerd omdat ze nu ingebouwd zijn in deze versie van Mattermost." + "id": "model.config.is_valid.elastic_search.ignored_indexes_dash_prefix.app_error", + "translation": "Genegeerde indexen voor purge mogen niet beginnen met een streepje." }, { - "id": "app.user.run.add_to_timeline.summary", - "translation": "Samenvatting" + "id": "api.post.post_priority.persistent_notification_validation_error.request_error", + "translation": "Validatie van blijvende meldingen is mislukt." }, { - "id": "app.user.run.add_to_timeline.submit_label", - "translation": "Toevoegen aan de uitvoeringstijdlijn" - }, - { - "id": "app.user.run.update_status.reminder_for_next_update", - "translation": "Herinnering voor de volgende update" - }, - { - "id": "app.user.run.update_status.num_channel", - "translation": { - "one": "Geef een update aan de belanghebbenden. Dit bericht zal verstuurd worden naar {{.Count}} kanaal.", - "other": "Geef een update aan de belanghebbenden. Dit bericht zal verstuurd worden naar {{.Count}} kanalen." - } - }, - { - "id": "app.user.run.update_status.finish_run.placeholder", - "translation": "Markeer ook de uitvoering als beëindigd" - }, - { - "id": "app.user.run.update_status.finish_run", - "translation": "Uitvoering beëindigen" - }, - { - "id": "app.user.run.update_status.change_since_last_update", - "translation": "Wijziging sinds laatste update" + "id": "api.post.post_priority.priority_post_not_allowed_for_user.request_error", + "translation": "Gebruiker mag geen prioriteitspost of blijvende melding aanmaken." }, { "id": "app.user.run.status_enable", "translation": "@{.Username}} heeft de statusupdates voor [{{.RunName}}]({{.RunURL}}) uitgeschakeld" }, - { - "id": "app.user.run.status_disable", - "translation": "@{{.Username}} heeft de statusupdates voor [{{.RunName}}]({{.RunURL}}) uitgeschakeld" - }, - { - "id": "app.user.run.request_update", - "translation": "@here - @{{.Name}} vroeg een statusupdate aan voor [{{.RunName}}]({{.RunURL}}). \n" - }, - { - "id": "app.user.run.request_join_channel", - "translation": "@{{.Name}} is een deelnemer aan de uitvoering en wil lid worden van dit kanaal. Elk lid van het kanaal kan hen uitnodigen.\n" - }, - { - "id": "app.user.run.confirm_finish.title", - "translation": "Bevestig beëindigen uitvoering" - }, - { - "id": "app.user.run.confirm_finish.submit_label", - "translation": "Uitvoering beëindigen" - }, - { - "id": "app.user.run.confirm_finish.num_outstanding", - "translation": { - "one": "Er is **{.Count}} openstaande taak**. Weet je zeker dat je de uitvoering *{.RunName}}* voor alle deelnemers wilt beëindigen?", - "other": "Er is **{.Count}} openstaande taken**. Weet je zeker dat je de uitvoering *{.RunName}}* voor alle deelnemer wilt beëindigen?" - } - }, - { - "id": "app.user.run.add_to_timeline.title", - "translation": "Toevoegen aan de uitvoeringstijdlijn" - }, { "id": "app.user.run.add_to_timeline.summary.placeholder", "translation": "Korte samenvatting weergeven in de tijdlijn" }, { - "id": "app.user.run.add_to_timeline.summary.help", - "translation": "Maximaal 64 tekens" + "id": "model.config.is_valid.elastic_search.ignored_indexes_dash_prefix.app_error", + "translation": "Genegeerde indexen voor purge mogen niet beginnen met een streepje." + }, + { + "id": "api.post.post_priority.persistent_notification_validation_error.request_error", + "translation": "Validatie van blijvende meldingen is mislukt." + }, + { + "id": "api.post.post_priority.priority_post_not_allowed_for_user.request_error", + "translation": "Gebruiker mag geen prioriteitspost of blijvende melding aanmaken." + }, + { + "id": "api.post.post_priority.priority_post_only_allowed_for_root_post.request_error", + "translation": "Alleen berichten die een draadje beginnen mogen prioriteit hebben." + }, + { + "id": "api.post.post_priority.urgent_persistent_notification_post.request_error", + "translation": "Blijvende berichten moeten de prioriteit Dringend hebben." + }, + { + "id": "api.command_exportlink.driver.app_error", + "translation": "Het stuurprogramma voor bestandsopslag ondersteunt het genereren van snelkoppelingen niet." + }, + { + "id": "api.command_exportlink.link.text", + "translation": "Je kan jouw bestand hier downloaden: {{.Link}}.\nDeze link verloopt over {{.Expiration}}." + }, + { + "id": "app.post_persistent_notification.delete_by_channel.app_error", + "translation": "Kan de blijvende meldingen per kanaal niet verwijderen." + }, + { + "id": "app.post_priority.delete_persistent_notification_post.app_error", + "translation": "Blijvend bericht kon niet worden verwijderd" + }, + { + "id": "api.post.post_priority.max_recipients_persistent_notification_post.request_error", + "translation": "Berichten met een blijvende meldingen laten maximaal {{.MaxRecipients}} ontvangers toe." + }, + { + "id": "model.config.is_valid.persistent_notifications_count.app_error", + "translation": "Ongeldig totaal aantal blijvende meldingen per bericht. Moet een positief getal zijn." + }, + { + "id": "model.config.is_valid.persistent_notifications_interval.app_error", + "translation": "Ongeldige frequentie van blijvende meldingen. Moet minstens twee minuten zijn." + }, + { + "id": "ent.elasticsearch.max_version.app_error", + "translation": "Elasticsearch versie {{.Version}} is hoger dan de maximaal ondersteunde versie van {{.MaxVersion}}" + }, + { + "id": "api.license.true_up_review.failed_to_submit", + "translation": "Kon geen profiel met actueel aantal gebruikers versturen naar CWS." + }, + { + "id": "model.config.is_valid.persistent_notifications_recipients.app_error", + "translation": "Ongeldig maximum aantal ontvangers voor blijvende meldingen. Moet een positief getal zijn." + }, + { + "id": "app.post_persistent_notification.delete_by_team.app_error", + "translation": "Kan de blijvende meldingen per team niet verwijderen." + }, + { + "id": "model.channel_member.is_valid.notify_props.app_error", + "translation": "De limiet voor de grootte van props is overschreden." + }, + { + "id": "api.command_exportlink.desc", + "translation": "Genereer een link om een export te downloaden." + }, + { + "id": "api.command_exportlink.empty.app_error", + "translation": "Er is geen exportbestand gevonden." + }, + { + "id": "api.command_exportlink.hint", + "translation": "[job-id|zip bestandsnaam|{{.LatestMsg}}]" + }, + { + "id": "api.command_exportlink.invalid.app_error", + "translation": "Het gevraagde bestand kon niet worden gevonden." + }, + { + "id": "api.command_exportlink.list.app_error", + "translation": "Kan de exportlijst niet ophalen." + }, + { + "id": "api.command_exportlink.name", + "translation": "exportlink" + }, + { + "id": "api.command_exportlink.permission.app_error", + "translation": "Je hebt niet genoeg rechten om dit commando uit te voeren." + }, + { + "id": "api.command_exportlink.presign.app_error", + "translation": "Kan geen presign-url genereren." + }, + { + "id": "api.user.invalidate_password_recovery_tokens.error", + "translation": "Kan geen tokens per type krijgen bij het ongeldig maken van wachtwoordherstel tokens" + }, + { + "id": "api.user.invalidate_password_recovery_tokens_parse.error", + "translation": "Kan token niet parsen bij het ongeldig maken van tokens voor wachtwoordherstel" + }, + { + "id": "app.channel.get_channels_member_count.existing.app_error", + "translation": "Kan aantal leden voor opgegeven kanalen niet ophalen." + }, + { + "id": "app.channel.get_channels_member_count.find.app_error", + "translation": "Fout bij het ophalen van het aantal leden." + }, + { + "id": "app.channel.update_member.app_error", + "translation": "Kan het kanaallid niet bijwerken." + }, + { + "id": "app.channel.update_member.notify_props_limit_exceeded.app_error", + "translation": "Kan kanaallid niet bijwerken, meldings propsgroottte limiet overschreden." + }, + { + "id": "app.eport.generate_presigned_url.config.app_error", + "translation": "Deze actie vereist het gebruik van een speciale exportstore." + }, + { + "id": "app.eport.generate_presigned_url.driver.app_error", + "translation": "Het stuurprogramma van je exportwinkel ondersteunt het genereren van presign-url's niet." + }, + { + "id": "app.eport.generate_presigned_url.featureflag.app_error", + "translation": "Deze functie wordt beperkt door een feature flag." + }, + { + "id": "app.eport.generate_presigned_url.fileexist.app_error", + "translation": "Kon niet controleren of het bestand bestaat." + }, + { + "id": "app.eport.generate_presigned_url.link.app_error", + "translation": "Kan de presigned url niet genereren." + }, + { + "id": "app.eport.generate_presigned_url.notfound.app_error", + "translation": "Het exportbestand is niet gevonden." + }, + { + "id": "app.post.delete_post.get_team.app_error", + "translation": "Er is een fout opgetreden bij het ophalen van het team." + }, + { + "id": "model.config.is_valid.local_mode_socket.app_error", + "translation": "Kan lokale socket-bestandsmap niet vinden." + }, + { + "id": "api.error_no_organization_name_provided_for_self_hosted_onboarding", + "translation": "Fout geen organisatienaam opgegeven bij self hosted introductie." + }, + { + "id": "app.file_info.get.gif.app_error", + "translation": "Kan gif niet decoderen." + }, + { + "id": "model.channel_member.is_valid.channel_auto_follow_threads_value.app_error", + "translation": "Ongeldige kanaal-automatisch-draadjes-volgen-waarde." + }, + { + "id": "model.license_record.is_valid.bytes.app_error", + "translation": "Ongeldige waarde voor bytes bij het uploaden van een licentie." + }, + { + "id": "app.save_config.plugin_hook_error", + "translation": "Er is een fout opgetreden bij het uitvoeren van de pluginhook bij het opslaan van de configuratie." + }, + { + "id": "api.user.invalidate_password_recovery_tokens_delete.error", + "translation": "Kan token niet verwijderen bij het ongeldig maken van tokens voor wachtwoordherstel" + }, + { + "id": "api.post.post_priority.min_recipients_persistent_notification_post.request_error", + "translation": "Berichten met een blijvende meldingen moeten minimaal {{.MaxRecipients}} 1 ontvanger hebben." } ] diff --git a/server/i18n/pl.json b/server/i18n/pl.json index 062568ddc78..97908d2bef2 100644 --- a/server/i18n/pl.json +++ b/server/i18n/pl.json @@ -4043,10 +4043,6 @@ "id": "model.emoji.user_id.app_error", "translation": "Nieprawidłowy creator id." }, - { - "id": "model.file_info.get.gif.app_error", - "translation": "Nie można zdekodować gifa." - }, { "id": "model.file_info.is_valid.create_at.app_error", "translation": "Nieprawidłowa wartość dla create_at." @@ -4757,7 +4753,7 @@ }, { "id": "web.error.unsupported_browser.min_browser_version.edge", - "translation": "Wersja 110+" + "translation": "Wersja 112+" }, { "id": "web.error.unsupported_browser.min_browser_version.chrome", @@ -9107,10 +9103,6 @@ "id": "api.team.invite_members_to_team_and_channels.invalid_body.app_error", "translation": "Nieprawidłowa treść żądania." }, - { - "id": "api.insights.feature_disabled", - "translation": "Insights kryje się za flagą funkcji, która nie jest włączona." - }, { "id": "api.templates.cloud_upgrade_confirmation.title", "translation": "Zostałeś uaktualniony!" @@ -9127,10 +9119,6 @@ "id": "api.file.cloud_upload.app_error", "translation": "Przesyłanie danych do instancji Chmury za pomocą mmctl nie jest obsługiwane. Proszę sprawdzić dokumentację tutaj: https://docs.mattermost.com/manage/cloud-data-export.html." }, - { - "id": "app.recent_searches.app_error", - "translation": "Błąd pobierania ostatnich wyszukiwań" - }, { "id": "app.teams.analytics_teams_count.app_error", "translation": "Nie można uzyskać liczby zespołów" @@ -9155,18 +9143,6 @@ "id": "model.config.is_valid.image_decoder_concurrency.app_error", "translation": "Nieprawidłowa współbieżność dekodera {{.Wartość}}. Powinna być liczbą dodatnią lub -1." }, - { - "id": "app.post.get_top_threads_for_user_since.app_error", - "translation": "Nie można uzyskać informacji o najważniejszych wątkach dla użytkownika." - }, - { - "id": "app.post.get_top_threads_for_team_since.app_error", - "translation": "Nie można uzyskać najlepszych wątków dla zespołu." - }, - { - "id": "app.insights.feature_disabled", - "translation": "Funkcja Insights jest wyłączona." - }, { "id": "api.cloud.notify_admin_to_upgrade_error.already_notified", "translation": "Administrator został już powiadomiony" @@ -9199,10 +9175,6 @@ "id": "app.team.clear_cache.app_error", "translation": "Błąd czyszczenia pamięci podręcznej członków zespołu" }, - { - "id": "app.post.get_top_dms_for_user_since.app_error", - "translation": "Nie można uzyskać najlepszych DM dla użytkownika." - }, { "id": "api.command_marketplace.unsupported.app_error", "translation": "Polecenie marketplace nie jest obsługiwane w Twoim urządzeniu." @@ -9467,14 +9439,6 @@ "id": "api.team.invite_guests_to_channels.disabled.error", "translation": "Konta gości są wyłączone" }, - { - "id": "app.collection.add_topic.exists.app_error", - "translation": "Typ tematu już istnieje." - }, - { - "id": "app.collection.add_collection.exists.app_error", - "translation": "Typ kolekcji już istnieje." - }, { "id": "api.user.add_user_to_group_syncables.not_ldap_user.app_error", "translation": "nie jest użytkownikiem ldap" @@ -9599,22 +9563,10 @@ "id": "api.acknowledgement.delete.archived_channel.app_error", "translation": "Nie można usunąć potwierdzenia w zarchiwizowanym kanale." }, - { - "id": "worktemplate.category.product_teams", - "translation": "Zespoły Produkcyjne" - }, { "id": "model.draft.is_valid.priority.app_error", "translation": "Nieprawidłowy priorytet" }, - { - "id": "app.worktemplates.get_templates.app_error", - "translation": "Nie można uzyskać szablonów roboczych" - }, - { - "id": "app.worktemplates.get_categories.app_error", - "translation": "Nie można uzyskać kategorii szablonów roboczych" - }, { "id": "api.user.get_users.validation.app_error", "translation": "Błąd pobierania ról podczas sprawdzania poprawności." @@ -9695,70 +9647,6 @@ "id": "app.group.username_conflict", "translation": "użytkownik o nazwie \"{{.Username}}\" już istnieje." }, - { - "id": "worktemplate.category.leadership", - "translation": "Przywództwo" - }, - { - "id": "worktemplate.category.devops", - "translation": "DevOps" - }, - { - "id": "worktemplate.category.companywide", - "translation": "Firma - Szeroka" - }, - { - "id": "app.worktemplates.execute_work_template.playbooks.find_channel_error", - "translation": "Nie można znaleźć kanału powiązanego z playbookiem." - }, - { - "id": "app.worktemplates.execute_work_template.playbooks.create_error", - "translation": "Błąd podczas tworzenia playbooka." - }, - { - "id": "app.worktemplates.execute_work_template.channels.create_error", - "translation": "Błąd podczas tworzenia kanału." - }, - { - "id": "app.worktemplates.execute_work_template.boards.create_error", - "translation": "Błąd podczas tworzenia tablicy." - }, - { - "id": "app.worktemplates.execute_work_template.app_error", - "translation": "Błąd podczas wykonywania szablonu roboczego." - }, - { - "id": "app.worktemplate.execution_request.license_cannot_create_private_playbook", - "translation": "Twoja licencja nie obsługuje prywatnych playbooków." - }, - { - "id": "app.worktemplate.execution_request.cannot_create_public_playbook", - "translation": "Nie masz uprawnień do tworzenia publicznego playbooka." - }, - { - "id": "app.worktemplate.execution_request.cannot_create_public_channel", - "translation": "Nie masz uprawnień do tworzenia kanału publicznego." - }, - { - "id": "app.worktemplate.execution_request.cannot_create_public_board", - "translation": "Nie masz uprawnień do tworzenia tablic publicznych." - }, - { - "id": "app.worktemplate.execution_request.cannot_create_private_playbook", - "translation": "Nie masz uprawnień do tworzenia prywatnego playbooka." - }, - { - "id": "app.worktemplate.execution_request.cannot_create_private_channel", - "translation": "Nie masz uprawnień do tworzenia kanału prywatnego." - }, - { - "id": "app.worktemplate.execution_request.cannot_create_private_board", - "translation": "Nie masz uprawnień do tworzenia prywatnej tablicy." - }, - { - "id": "api.work_templates.disabled", - "translation": "Szablony robocze są wyłączone." - }, { "id": "api.config.update_config.translations.app_error", "translation": "Nie udało się zaktualizować tłumaczeń serwera." @@ -9771,46 +9659,6 @@ "id": "api.server.cws.needs_enterprise_edition", "translation": "Usługa dostępna tylko w edycji Mattermost Enterprise" }, - { - "id": "api.command_templates.unsupported.app_error", - "translation": "Polecenie szablony nie jest obsługiwane w Twoim urządzeniu." - }, - { - "id": "api.command_templates.name", - "translation": "szablony" - }, - { - "id": "api.command_templates.desc", - "translation": "Otwórz okno tworzenia z szablonu" - }, - { - "id": "app.worktemplates.execute_work_template.name_too_long", - "translation": "Pole nazwa nie może zawierać więcej niż 64 znaki." - }, - { - "id": "app.team.create_onboarding_linked_board.product_key_not_found", - "translation": "nie znaleziono klucza produktu" - }, - { - "id": "app.team.create_onboarding_linked_board.itf_not_of_type", - "translation": "interfejs nie jest oczekiwanym typem" - }, - { - "id": "app.team.create_onboarding_linked_board.error_patching_board", - "translation": "błąd podczas paczowania tablicy" - }, - { - "id": "app.team.create_onboarding_linked_board.error_no_board", - "translation": "błąd nie utworzono tablicy" - }, - { - "id": "app.team.create_onboarding_linked_board.error_getting_templates", - "translation": "błąd pobierania szablonów tablic" - }, - { - "id": "app.team.create_onboarding_linked_board.error_duplicating_board", - "translation": "błąd duplikowania tablicy" - }, { "id": "api.system.logs.invalidFilter", "translation": "Nieprawidłowy filtr dziennika" @@ -9827,238 +9675,10 @@ "id": "app.oauth.remove_auth_data_by_client_id.app_error", "translation": "Nie można usunąć danych oauth." }, - { - "id": "app.user.digest.runs_in_progress.heading", - "translation": "Uruchomienia w Trakcie" - }, - { - "id": "app.user.digest.overdue_status_updates.zero_overdue", - "translation": "Masz 0 zaległych uruchomień." - }, - { - "id": "app.user.digest.overdue_status_updates.num_overdue", - "translation": { - "few": "Masz {{.Count}} zaległości w aktualizacji statusu:", - "many": "Masz {{.Count}} zaległości w aktualizacji statusu:", - "one": "Masz {{.Count}} zaległość w aktualizacji statusu:" - } - }, - { - "id": "app.user.digest.overdue_status_updates.heading", - "translation": "Zaległe aktualizacje statusu" - }, - { - "id": "app.command.execute.error", - "translation": "Nie można wykonać polecenia." - }, - { - "id": "app.user.digest.runs_in_progress.zero_in_progress", - "translation": "Obecnie masz 0 uruchomień w trakcie." - }, - { - "id": "app.user.digest.tasks.all_tasks_command", - "translation": "Użyj `/playbook todo`, aby zobaczyć wszystkie swoje zadania." - }, - { - "id": "app.user.digest.tasks.heading", - "translation": "Twoje przydzielone zadania" - }, - { - "id": "app.user.digest.tasks.zero_assigned", - "translation": "Masz 0 przydzielonych zadań." - }, - { - "id": "app.user.new_run.run_name", - "translation": "Nazwa uruchomienia" - }, - { - "id": "app.user.new_run.title", - "translation": "Uruchom playbook" - }, - { - "id": "app.user.run.add_checklist_item.description", - "translation": "Opis" - }, - { - "id": "app.user.run.add_checklist_item.name", - "translation": "Nazwa" - }, - { - "id": "app.user.run.add_checklist_item.submit_label", - "translation": "Dodaj zadanie" - }, - { - "id": "app.user.run.add_checklist_item.title", - "translation": "Dodaj nowe zadanie" - }, - { - "id": "app.user.run.add_to_timeline.playbook_run", - "translation": "Uruchomienie Playbooka" - }, - { - "id": "app.user.run.add_to_timeline.submit_label", - "translation": "Dodaj do osi czasu uruchomienia" - }, - { - "id": "app.user.run.add_to_timeline.summary", - "translation": "Podsumowanie" - }, - { - "id": "app.user.run.add_to_timeline.summary.help", - "translation": "Maksymalnie 64 znaki" - }, - { - "id": "app.user.run.add_to_timeline.summary.placeholder", - "translation": "Krótkie podsumowanie widoczne na osi czasu" - }, - { - "id": "app.user.run.update_status.finish_run", - "translation": "Zakończ uruchomienie" - }, - { - "id": "app.user.run.update_status.finish_run.placeholder", - "translation": "Oznacz również uruchomienie jako zakończone" - }, - { - "id": "app.user.run.update_status.reminder_for_next_update", - "translation": "Przypomnienie o następnej aktualizacji" - }, - { - "id": "app.user.run.update_status.submit_label", - "translation": "Status aktualizacji" - }, - { - "id": "app.user.digest.runs_in_progress.num_in_progress", - "translation": { - "few": "Masz {{.Count}} uruchomienia w toku:", - "many": "Masz {{.Count}} uruchomień w toku:", - "one": "Masz {{.Count}} uruchomienie w toku:" - } - }, - { - "id": "app.user.digest.tasks.due_in_x_days", - "translation": { - "few": "Termin płatności za {{.Count}} dni", - "many": "Termin płatności za {{.Count}} dni", - "one": "Termin płatności za {{.Count}} dzień" - } - }, - { - "id": "app.user.digest.tasks.due_after_today", - "translation": { - "few": "Masz **{{.Count}} przydzielone zadania, których termin wykonania wypada po dzisiejszym dniu**.", - "many": "Masz **{{.Count}} przydzielonych zadań, których termin wykonania wypada po dzisiejszym dniu**.", - "one": "Masz **{{.Count}} przydzielone zadanie, którego termin wykonania wypada po dzisiejszym dniu**." - } - }, - { - "id": "app.user.digest.tasks.due_today", - "translation": "Do zapłaty dzisiaj" - }, - { - "id": "app.user.digest.tasks.due_x_days_ago", - "translation": "Termin {{.Count}} dni temu" - }, - { - "id": "app.user.digest.tasks.num_assigned", - "translation": { - "few": "Masz {{.Count}} przydzielone zadania:", - "many": "Masz {{.Count}} przydzielonych zadań:", - "one": "Masz {{.Count}} przydzielone zadanie:" - } - }, - { - "id": "app.user.digest.tasks.num_assigned_due_until_today", - "translation": { - "few": "Masz {{.Count}} przydzielone zadania, których termin wykonania właśnie upływa:", - "many": "Masz {{.Count}} przydzielonych zadań, których termin wykonania właśnie upływa:", - "one": "Masz {{.Count}} przydzielone zadanie, którego termin wykonania właśnie upływa:" - } - }, { "id": "app.user.new_run.intro", "translation": "**Właściciel** {{.Username}}" }, - { - "id": "app.user.new_run.playbook", - "translation": "Playbook" - }, - { - "id": "app.user.new_run.submit_label", - "translation": "Rozpocznij uruchomienie" - }, - { - "id": "app.user.run.confirm_finish.num_outstanding", - "translation": { - "few": "Są **{.Count}} zaległe zadania**. Czy na pewno chcesz zakończyć uruchomienie *{.RunName}}* dla wszystkich uczestników?", - "many": "Jest **{.Count}} zaległych zadań**. Czy na pewno chcesz zakończyć uruchomienie *{.RunName}}* dla wszystkich uczestników?", - "one": "Jest **{.Count}} zaległe zadanie**. Czy na pewno chcesz zakończyć uruchomienie *{.RunName}}* dla wszystkich uczestników?" - } - }, - { - "id": "app.user.run.update_status.num_channel", - "translation": { - "few": "Przedstaw aktualizację dla interesariuszy. Ten post będzie transmitowany na {{.Count}} kanałach.", - "many": "Przedstaw aktualizację dla interesariuszy. Ten post będzie transmitowany na {{.Count}} kanałach.", - "one": "Przedstaw aktualizację dla interesariuszy. Ten post będzie transmitowany na {{.Count}} kanale." - } - }, - { - "id": "app.user.run.update_status.title", - "translation": "Aktualizacje statusu" - }, - { - "id": "app.user.digest.tasks.due_yesterday", - "translation": "Termin na wczoraj" - }, - { - "id": "app.user.run.status_disable", - "translation": "@{.Username}} wyłączył aktualizacje statusu dla [{{.RunName}}]({{.RunURL}})" - }, - { - "id": "app.user.run.status_enable", - "translation": "@{.Username}} włączył aktualizacje statusu dla [{{.RunName}}]({{.RunURL}})" - }, - { - "id": "app.user.run.update_status.change_since_last_update", - "translation": "Zmiana od ostatniej aktualizacji" - }, - { - "id": "app.user.run.confirm_finish.submit_label", - "translation": "Zakończ uruchomienie" - }, - { - "id": "app.user.run.confirm_finish.title", - "translation": "Potwierdź zakończenie uruchomienia" - }, - { - "id": "app.user.run.request_join_channel", - "translation": "@{{.Name}} jest uczestnikiem uruchomienia i chce dołączyć do tego kanału. Każdy członek kanału może go zaprosić.\n" - }, - { - "id": "app.user.run.request_update", - "translation": "@here - @{.Name}} zażądał aktualizacji statusu dla [{{.RunName}}]({{.RunURL}}). \n" - }, - { - "id": "app.user.run.add_to_timeline.title", - "translation": "Dodaj do osi czasu uruchomienia" - }, - { - "id": "worktemplate.companywide.goals_and_okrs.board", - "translation": "Śledź postępy swojego zespołu w realizacji celów organizacji dzięki tablicy Cele i OKR. Utrzymuj spotkania na właściwym torze dzięki tablicy Agenda spotkań." - }, - { - "id": "worktemplate.companywide.create_project.integration", - "translation": "Zwiększ produktywność na swoim kanale, integrując najczęściej używane narzędzia. Zostaną one pobrane za Ciebie." - }, - { - "id": "worktemplate.companywide.create_project.channel", - "translation": "Porozmawiaj ze swoim zespołem o nowym projekcie i zdecyduj, jak zamierzasz go ustrukturyzować, na kanale współpracy." - }, - { - "id": "worktemplate.companywide.create_project.board", - "translation": "Użyj tablicy Kanban, aby zdefiniować i śledzić zadania i postępy w projekcie." - }, { "id": "model.license_record.is_valid.bytes.app_error", "translation": "Nieprawidłowa wartość dla bajtów podczas wgrywania licencji." @@ -10071,10 +9691,6 @@ "id": "app.plugin.skip_installation.app_error", "translation": "Pomijanie instalacji wtyczki {{.Id}}, ponieważ istniejąca wersja jest równa lub nowsza." }, - { - "id": "app.plugin.blocked.app_error", - "translation": "Wtyczka {{.Id}} jest na liście blokowanych. Niektóre wtyczki są zablokowane, ponieważ są wbudowane w tę wersję Mattermost." - }, { "id": "api.server.cws.subscribe_to_newsletter.app_error", "translation": "Serwer CWS nie zdołał zapisać się do newslettera." @@ -10084,107 +9700,179 @@ "translation": "Nieprawidłowy e-mail służbowy dla wersji testowej" }, { - "id": "worktemplate.product_teams.sprint_planning.integration", - "translation": "Zwiększ produktywność swojego kanału poprzez integrację najczęściej używanych narzędzi, takich jak Zoom. Zostaną one pobrane za Ciebie." + "id": "model.channel_member.is_valid.channel_auto_follow_threads_value.app_error", + "translation": "Nieprawidłowa wartość channel-auto-follow-threads." }, { - "id": "worktemplate.product_teams.sprint_planning.channel", - "translation": "Czatuj ze swoim zespołem w kanale, który łatwo łączy się z Twoimi tablicami i integracjami." + "id": "model.config.is_valid.persistent_notifications_count.app_error", + "translation": "Nieprawidłowa całkowita liczba trwałych powiadomień na post. Musi być liczbą dodatnią." }, { - "id": "worktemplate.product_teams.sprint_planning.board", - "translation": "Śledź postępy swojego zespołu w realizacji tygodniowych celów dzięki podziałom na sprinty, ustalaniu priorytetów, przypisywaniu właścicieli i komentarzom." + "id": "model.config.is_valid.persistent_notifications_interval.app_error", + "translation": "Nieprawidłowa częstotliwość trwałych powiadomień. Musi wynosić co najmniej dwie minuty." }, { - "id": "worktemplate.product_teams.product_roadmap.channel", - "translation": "Porozmawiaj ze swoim zespołem o opiniach klientów, ustal priorytety i wspólnie ustalcie postępy." + "id": "api.post.post_priority.max_recipients_persistent_notification_post.request_error", + "translation": "Trwałe powiadomienie pozwala na maksymalnie {{.MaxRecipients}} odbiorców." }, { - "id": "worktemplate.product_teams.product_roadmap.board", - "translation": "Wykorzystaj tablicę Product Roadmap do zarządzania opiniami użytkowników, przydzielania zasobów, przeglądania zadań w widoku kalendarza i nadawania priorytetów." + "id": "api.post.post_priority.persistent_notification_validation_error.request_error", + "translation": "Walidacja trwałego powiadomienia nie powiodła się." }, { - "id": "worktemplate.product_teams.goals_and_okrs.integration", - "translation": "Zwiększ produktywność swojego kanału, integrując najczęściej używane narzędzia, takie jak Zoom, aby ułatwić współpracę. Zostaną one pobrane za Ciebie." + "id": "api.post.post_priority.priority_post_only_allowed_for_root_post.request_error", + "translation": "Tylko posty root mogą mieć priorytet." }, { - "id": "worktemplate.product_teams.goals_and_okrs.channel", - "translation": "Czatuj o swoich celach i postępach ze swoim zespołem, asynchronicznie lub w czasie rzeczywistym, i bądź na bieżąco ze zmianami na jednym kanale." + "id": "api.post.post_priority.urgent_persistent_notification_post.request_error", + "translation": "Posty z powiadomieniami stałymi muszą mieć priorytet Pilne." }, { - "id": "worktemplate.product_teams.goals_and_okrs.board", - "translation": "Śledź postępy swojego zespołu w realizacji celów organizacyjnych dzięki tablicy Cele i OKR. Utrzymuj spotkania na właściwym torze dzięki tablicy Agenda spotkań." + "id": "api.command_exportlink.desc", + "translation": "Wygeneruj link do pobrania eksportu." }, { - "id": "worktemplate.product_teams.feature_release.description.playbook", - "translation": "Usprawnij współpracę międzyfunkcyjnego zespołu dzięki listom kontrolnym zadań i automatyzacji, które wspierają proces tworzenia funkcji. Kiedy skończysz, przeprowadź retrospektywę i wprowadź ulepszenia do następnego wydania." + "id": "api.command_exportlink.link.text", + "translation": "Plik można pobrać tutaj: {{.Link}}.\nTen link wygaśnie w {{.Expiration}}." }, { - "id": "worktemplate.product_teams.feature_release.description.integration", - "translation": "Zwiększ wydajność na swoim kanale, integrując najczęściej używane narzędzia do obsługi wydania funkcji, takie jak GitHub. Zostaną one pobrane za Ciebie." + "id": "api.post.post_priority.priority_post_not_allowed_for_user.request_error", + "translation": "Użytkownik nie może tworzyć priorytetowych postów ani trwałych powiadomień." }, { - "id": "worktemplate.product_teams.feature_release.description.channel", - "translation": "Czatuj ze swoim zespołem na temat wszelkich blokad wydania i zmian w kanale, który łatwo łączy się z tablicami, playbookami i innymi integracjami." + "id": "app.post_persistent_notification.delete_by_team.app_error", + "translation": "Nie można usunąć trwałych powiadomień z zespołu." }, { - "id": "worktemplate.product_teams.feature_release.description.board", - "translation": "Utrzymuj spotkania na właściwym torze dzięki tablicy Agenda spotkań. Zarządzaj swoim obciążeniem pracą dzięki tablicy Zadania projektowe." + "id": "model.config.is_valid.elastic_search.ignored_indexes_dash_prefix.app_error", + "translation": "Ignorowane indeksy nie powinny zaczynać się od myślnika." }, { - "id": "worktemplate.product_teams.bug_bash.playbook", - "translation": "Użyj list kontrolnych, aby przypisać obszary testowe i zautomatyzowane zadania, aby uruchomić kompleksowy proces bug bash. Użyj retrospektywy, aby przejrzeć swój proces i poprawić go na następny raz." + "id": "api.command_exportlink.driver.app_error", + "translation": "Sterownik magazynu plików nie obsługuje generowania linków." }, { - "id": "worktemplate.product_teams.bug_bash.integration", - "translation": "Zwiększ produktywność w swoim kanale, integrując swoje najczęściej używane narzędzia, takie jak Jira, aby śledzić postępy w bug bashu. Zostaną one pobrane za Ciebie." + "id": "api.command_exportlink.empty.app_error", + "translation": "Nie znaleziono pliku eksportu." }, { - "id": "worktemplate.product_teams.bug_bash.channel", - "translation": "Planuj i zarządzaj raportami o błędach i rozwiązaniami w jednym kanale, który jest łatwo dostępny dla Twojego zespołu i organizacji." + "id": "api.command_exportlink.hint", + "translation": "[job-id|plik zip|{{.LatestMsg}}]" }, { - "id": "worktemplate.leadership.goals_and_okrs.integration", - "translation": "Zwiększ produktywność swojego kanału, integrując najczęściej używane narzędzia, takie jak Zoom, aby ułatwić współpracę. Zostaną one pobrane za Ciebie." + "id": "api.command_exportlink.invalid.app_error", + "translation": "Nie można znaleźć żądanego pliku." }, { - "id": "worktemplate.leadership.goals_and_okrs.channel", - "translation": "Czatuj o swoich celach i postępach ze swoim zespołem, asynchronicznie lub w czasie rzeczywistym, i bądź na bieżąco ze zmianami w jednym kanale." + "id": "api.command_exportlink.list.app_error", + "translation": "Nie można pobrać listy eksportu." }, { - "id": "worktemplate.leadership.goals_and_okrs.board", - "translation": "Śledź postępy swojego zespołu w realizacji celów organizacyjnych dzięki tablicy Cele i OKR. Utrzymuj spotkania na właściwym torze dzięki tablicy Agenda spotkań." + "id": "api.command_exportlink.name", + "translation": "exportlink" }, { - "id": "worktemplate.devops.product_release.playbook", - "translation": "Stwórz powtarzalne przepływy pracy, które są łatwe do naśladowania i wdrożenia, aby wydania produktów były niezawodne i terminowe." + "id": "api.command_exportlink.permission.app_error", + "translation": "Nie masz wystarczających uprawnień, aby uruchomić to polecenie." }, { - "id": "worktemplate.devops.product_release.channel", - "translation": "Rozmawiaj ze swoim zespołem o codziennych kamieniach milowych, wszelkich blokadach i zmianach w produktach - łatwo i szybko." + "id": "api.command_exportlink.presign.app_error", + "translation": "Nie można wygenerować adresu url." }, { - "id": "worktemplate.devops.product_release.board", - "translation": "Użyj tablicy Product Release, aby wesprzeć ramy czasowe i proces wydawania produktów, upewniając się, że wszyscy wiedzą, które zadania są wymagalne." + "id": "app.channel.get_channels_member_count.existing.app_error", + "translation": "Nie można znaleźć liczby członków dla podanych kanałów." }, { - "id": "worktemplate.devops.incident_resolution.description.playbook", - "translation": "Użyj list kontrolnych i automatyzacji, aby wprowadzić kluczowych członków zespołu, i podziel się tym, jak twój incydent jest śledzony w kierunku rozwiązania." + "id": "app.channel.get_channels_member_count.find.app_error", + "translation": "Nie można znaleźć liczby członków." }, { - "id": "worktemplate.devops.incident_resolution.description.channel", - "translation": "Rozmawiaj ze swoim zespołem o priorytetach, dodawaj interesariuszy, dostarczaj aktualizacje i pracuj nad rozwiązaniem na jednym kanale." + "id": "app.channel.update_member.app_error", + "translation": "Nie można zaktualizować członka kanału." }, { - "id": "worktemplate.devops.incident_resolution.description.board", - "translation": "Użyj tablicy rozwiązywania incydentów, aby wspierać powtarzalne procesy i przypisywać zdefiniowane zadania w całym zespole." + "id": "app.channel.update_member.notify_props_limit_exceeded.app_error", + "translation": "Nie można zaktualizować członka kanału, przekroczono limit rozmiaru powiadomień." }, { - "id": "worktemplate.companywide.goals_and_okrs.integration", - "translation": "Zwiększ produktywność swojego kanału, integrując najczęściej używane narzędzia, takie jak Zoom, aby ułatwić współpracę. Zostaną one pobrane za Ciebie." + "id": "app.eport.generate_presigned_url.config.app_error", + "translation": "Działania te wymagają użycia dedykowanego magazynu eksportu." }, { - "id": "worktemplate.companywide.goals_and_okrs.channel", - "translation": "Czatuj o swoich celach i postępach ze swoim zespołem, asynchronicznie lub w czasie rzeczywistym, i bądź na bieżąco ze zmianami w jednym kanale." + "id": "app.eport.generate_presigned_url.driver.app_error", + "translation": "Sterownik sklepu eksportowego nie obsługuje generowania adresów url presign." + }, + { + "id": "app.eport.generate_presigned_url.featureflag.app_error", + "translation": "Ta funkcja jest ograniczona przez flagę funkcji." + }, + { + "id": "app.eport.generate_presigned_url.fileexist.app_error", + "translation": "Nie można sprawdzić czy plik istnieje." + }, + { + "id": "app.eport.generate_presigned_url.link.app_error", + "translation": "Nie można wygenerować przypisanego adresu URL." + }, + { + "id": "app.eport.generate_presigned_url.notfound.app_error", + "translation": "Plik eksportu nie został znaleziony." + }, + { + "id": "model.channel_member.is_valid.notify_props.app_error", + "translation": "Przekroczenie limitu rozmiaru powiadomień." + }, + { + "id": "model.config.is_valid.local_mode_socket.app_error", + "translation": "Nie można zlokalizować lokalnego katalogu z plikami." + }, + { + "id": "app.save_config.plugin_hook_error", + "translation": "Wystąpił błąd podczas uruchamiania hooka wtyczki przy zapisywaniu konfiguracji." + }, + { + "id": "api.error_no_organization_name_provided_for_self_hosted_onboarding", + "translation": "Błąd brak podanej nazwy organizacji dla samodzielnego hostowania onboardingu." + }, + { + "id": "api.license.true_up_review.failed_to_submit", + "translation": "Nie udało się przesłać do CWS profilu true up review." + }, + { + "id": "api.user.invalidate_password_recovery_tokens.error", + "translation": "Nie można uzyskać tokenów według typu podczas unieważniania tokenów odzyskiwania hasła" + }, + { + "id": "api.user.invalidate_password_recovery_tokens_delete.error", + "translation": "Nie można usunąć tokena podczas unieważniania tokenów odzyskiwania hasła" + }, + { + "id": "api.user.invalidate_password_recovery_tokens_parse.error", + "translation": "Nie można przetworzyć tokena podczas unieważniania tokenów odzyskiwania hasła" + }, + { + "id": "app.file_info.get.gif.app_error", + "translation": "Nie można zdekodować gifa." + }, + { + "id": "app.post_persistent_notification.delete_by_channel.app_error", + "translation": "Nie można usunąć trwałych powiadomień z kanału." + }, + { + "id": "app.post_priority.delete_persistent_notification_post.app_error", + "translation": "Nie udało się usunąć trwałego wpisu powiadomienia" + }, + { + "id": "app.post.delete_post.get_team.app_error", + "translation": "Wystąpił błąd podczas pobierania zespołu." + }, + { + "id": "model.config.is_valid.persistent_notifications_recipients.app_error", + "translation": "Nieprawidłowa maksymalna liczba odbiorców trwałych powiadomień. Musi to być liczba dodatnia." + }, + { + "id": "api.post.post_priority.min_recipients_persistent_notification_post.request_error", + "translation": "Powiadomienie stałe musi mieć co najmniej 1 odbiorcę." } ] diff --git a/server/i18n/pr.json b/server/i18n/pr.json new file mode 100644 index 00000000000..0967ef424bc --- /dev/null +++ b/server/i18n/pr.json @@ -0,0 +1 @@ +{} diff --git a/server/i18n/pt-BR.json b/server/i18n/pt-BR.json index 9a5497d4b63..6ddaaa23dbd 100644 --- a/server/i18n/pt-BR.json +++ b/server/i18n/pt-BR.json @@ -4043,10 +4043,6 @@ "id": "model.emoji.user_id.app_error", "translation": "creator Id inválido." }, - { - "id": "model.file_info.get.gif.app_error", - "translation": "Não foi possível decodificar o gif." - }, { "id": "model.file_info.is_valid.create_at.app_error", "translation": "Valor inválido para create_at." diff --git a/server/i18n/pt.json b/server/i18n/pt.json new file mode 100644 index 00000000000..e5aafb92579 --- /dev/null +++ b/server/i18n/pt.json @@ -0,0 +1,58 @@ +[ + { + "id": "api.admin.add_certificate.open.app_error", + "translation": "Não foi possível abrir o ficheiro do certificado." + }, + { + "id": "api.admin.add_certificate.no_file.app_error", + "translation": "Nenhum ficheiro de 'certificado' no pedido." + }, + { + "id": "September", + "translation": "Setembro" + }, + { + "id": "October", + "translation": "Outubro" + }, + { + "id": "November", + "translation": "Novembro" + }, + { + "id": "May", + "translation": "Maio" + }, + { + "id": "March", + "translation": "Março" + }, + { + "id": "June", + "translation": "Junho" + }, + { + "id": "July", + "translation": "Julho" + }, + { + "id": "January", + "translation": "Janeiro" + }, + { + "id": "February", + "translation": "Fevereiro" + }, + { + "id": "December", + "translation": "Dezembro" + }, + { + "id": "August", + "translation": "Agosto" + }, + { + "id": "April", + "translation": "Abril" + } +] diff --git a/server/i18n/ro.json b/server/i18n/ro.json index 295891d6473..fc365aa4765 100644 --- a/server/i18n/ro.json +++ b/server/i18n/ro.json @@ -4043,10 +4043,6 @@ "id": "model.emoji.user_id.app_error", "translation": "Identitatea creatorului invalidă." }, - { - "id": "model.file_info.get.gif.app_error", - "translation": "Sa decodeze gif." - }, { "id": "model.file_info.is_valid.create_at.app_error", "translation": "Valoare invalidă pentru create_at." diff --git a/server/i18n/ru.json b/server/i18n/ru.json index bd1b5c309a9..574ed43c756 100644 --- a/server/i18n/ru.json +++ b/server/i18n/ru.json @@ -4043,10 +4043,6 @@ "id": "model.emoji.user_id.app_error", "translation": "Недопустимый идентификатор автора." }, - { - "id": "model.file_info.get.gif.app_error", - "translation": "Не удалось декодировать gif." - }, { "id": "model.file_info.is_valid.create_at.app_error", "translation": "Недопустимое значение для create_at." @@ -5841,7 +5837,7 @@ }, { "id": "web.error.unsupported_browser.min_browser_version.edge", - "translation": "Версия 110+" + "translation": "Версия 112+" }, { "id": "web.error.unsupported_browser.min_browser_version.firefox", @@ -9095,10 +9091,6 @@ "id": "api.license.request_trial_license.embargoed", "translation": "Нам не удалось обработать запрос из-за ограничений для стран, на которые распространяется эмбарго. [Подробнее читайте в нашей документации](https://mattermost.com/pl/limitations-for-embargoed-countries) или обращайтесь по адресу legal@mattermost.com, если у вас есть вопросы об экспортных ограничениях." }, - { - "id": "api.insights.feature_disabled", - "translation": "Insights находится за флагом функции, который не включен." - }, { "id": "api.getThreadsForUser.bad_only_params", "translation": "Параметры OnlyThreads и OnlyTotals для getThreadsForUser являются взаимоисключающими" @@ -9227,26 +9219,10 @@ "id": "app.team.clear_cache.app_error", "translation": "Ошибка при очистке кэша участников команды" }, - { - "id": "app.recent_searches.app_error", - "translation": "Ошибка при получении результатов недавнего поиска" - }, { "id": "app.post_reminder_dm", "translation": "Здравствуйте, вот ваше напоминание об этом сообщении от @{{.Username}}: {{.SiteURL}}/{{.TeamName}}/pl/{{.PostId}}" }, - { - "id": "app.post.get_top_threads_for_user_since.app_error", - "translation": "Не удалось получить главные темы для пользователя." - }, - { - "id": "app.post.get_top_threads_for_team_since.app_error", - "translation": "Не удалось получить главные темы для команды." - }, - { - "id": "app.post.get_top_dms_for_user_since.app_error", - "translation": "Невозможно получить топ личных сообщений для пользователя." - }, { "id": "app.post.cloud.get.app_error", "translation": "Не удается получить сообщение, так как оно превышает лимит облачного плана." @@ -9275,10 +9251,6 @@ "id": "app.job.error", "translation": "Ошибка во время выполнения задания." }, - { - "id": "app.insights.feature_disabled", - "translation": "Функция Insights отключена." - }, { "id": "app.file.cloud.get.app_error", "translation": "Невозможно получить файл, так как он превысил лимит облачного тарифного плана." @@ -9459,14 +9431,6 @@ "id": "api.templates.delinquency_30.bullet.files", "translation": "Файлы" }, - { - "id": "app.collection.add_topic.exists.app_error", - "translation": "Тип темы уже существует." - }, - { - "id": "app.collection.add_collection.exists.app_error", - "translation": "Тип коллекции уже существует." - }, { "id": "api.templates.delinquency_75.downgrade_to_free", "translation": "Понижение статуса до Cloud Free" @@ -9599,22 +9563,10 @@ "id": "api.acknowledgement.delete.archived_channel.app_error", "translation": "Вы не можете удалить подтверждение в архивированном канале." }, - { - "id": "worktemplate.category.product_teams", - "translation": "Продуктовые команды" - }, { "id": "model.draft.is_valid.priority.app_error", "translation": "Неверный приоритет" }, - { - "id": "app.worktemplates.get_templates.app_error", - "translation": "Невозможно получить рабочие шаблоны" - }, - { - "id": "app.worktemplates.get_categories.app_error", - "translation": "Невозможно получить категории рабочих шаблонов" - }, { "id": "api.templates.cloud_welcome_email.yearly_plan_button", "translation": "Просмотр счета-фактуры" @@ -9691,126 +9643,22 @@ "id": "api.templates.delinquency_45.subtitle2", "translation": "Пониженное рабочее пространство может негативно повлиять на критически важные рабочие процессы, интеграции и другие важные для бизнеса действия, выполняемые в вашем рабочем пространстве." }, - { - "id": "app.worktemplates.execute_work_template.playbooks.find_channel_error", - "translation": "Невозможно найти канал, связанный с сценарием." - }, - { - "id": "app.worktemplates.execute_work_template.playbooks.create_error", - "translation": "Ошибка при создании сценария." - }, - { - "id": "app.worktemplates.execute_work_template.channels.create_error", - "translation": "Ошибка при создании канала." - }, - { - "id": "app.worktemplates.execute_work_template.boards.create_error", - "translation": "Ошибка при создании доски." - }, - { - "id": "app.worktemplates.execute_work_template.app_error", - "translation": "Ошибка при выполнении рабочего шаблона." - }, - { - "id": "app.worktemplate.execution_request.cannot_create_public_playbook", - "translation": "У вас нет прав на создание публичного сценария." - }, - { - "id": "app.worktemplate.execution_request.cannot_create_public_channel", - "translation": "У вас нет прав на создание публичного канала." - }, - { - "id": "app.worktemplate.execution_request.cannot_create_public_board", - "translation": "У вас нет прав для создания публичной доски." - }, - { - "id": "app.worktemplate.execution_request.cannot_create_private_playbook", - "translation": "У вас нет прав для создания приватного сценария." - }, - { - "id": "app.worktemplate.execution_request.cannot_create_private_channel", - "translation": "У вас нет прав для создания приватного канала." - }, - { - "id": "app.worktemplate.execution_request.cannot_create_private_board", - "translation": "У вас нет прав для создания приватной доски." - }, { "id": "app.group.username_conflict", "translation": "пользователь с таким именем \"{{.Username}}\" уже существует." }, - { - "id": "api.work_templates.disabled", - "translation": "Рабочие шаблоны отключены." - }, { "id": "api.config.update_config.translations.app_error", "translation": "Не удалось обновить переводы сервера." }, - { - "id": "app.worktemplate.execution_request.license_cannot_create_private_playbook", - "translation": "Ваша лицензия не поддерживает приватные сценарии." - }, - { - "id": "worktemplate.category.companywide", - "translation": "Масштаб всей компании" - }, { "id": "app.import.validate_user_teams_import_data.invalid_auth_service.error", "translation": "Неверная служба авторизации: {{.AuthService}}" }, - { - "id": "worktemplate.category.leadership", - "translation": "Лидерство" - }, - { - "id": "worktemplate.category.devops", - "translation": "DevOps" - }, { "id": "api.server.cws.needs_enterprise_edition", "translation": "Услуга доступна только в редакции Mattermost Enterprise" }, - { - "id": "api.command_templates.unsupported.app_error", - "translation": "Команда шаблонов не поддерживается на вашем устройстве." - }, - { - "id": "api.command_templates.name", - "translation": "шаблоны" - }, - { - "id": "api.command_templates.desc", - "translation": "Откройте окно создания из шаблона" - }, - { - "id": "app.worktemplates.execute_work_template.name_too_long", - "translation": "Поле имени не может содержать более 64 символов." - }, - { - "id": "app.team.create_onboarding_linked_board.product_key_not_found", - "translation": "ключ продукта не найден" - }, - { - "id": "app.team.create_onboarding_linked_board.itf_not_of_type", - "translation": "интерфейс не ожидаемого типа" - }, - { - "id": "app.team.create_onboarding_linked_board.error_patching_board", - "translation": "ошибка при подключении доски" - }, - { - "id": "app.team.create_onboarding_linked_board.error_no_board", - "translation": "ошибка при создании доски" - }, - { - "id": "app.team.create_onboarding_linked_board.error_getting_templates", - "translation": "ошибка при получении шаблонов досок" - }, - { - "id": "app.team.create_onboarding_linked_board.error_duplicating_board", - "translation": "ошибка при дубликации доски" - }, { "id": "api.system.logs.invalidFilter", "translation": "Неверный фильтр журнала" @@ -9827,54 +9675,6 @@ "id": "app.oauth.remove_auth_data_by_client_id.app_error", "translation": "Невозможно удалить данные oauth." }, - { - "id": "app.user.digest.tasks.due_after_today", - "translation": { - "few": "У вас есть **{{.Count}} назначенных задачи, которые необходимо выполнить после сегодняшнего дня**.", - "many": "У вас есть **{{.Count}} назначенных задач, которые необходимо выполнить после сегодняшнего дня**.", - "one": "У вас есть **{{.Count}} назначенная задача, которую необходимо выполнить после сегодняшнего дня**." - } - }, - { - "id": "app.user.digest.tasks.all_tasks_command", - "translation": "Пожалуйста, используйте `/playbook todo`, чтобы увидеть все свои задачи." - }, - { - "id": "app.user.digest.runs_in_progress.zero_in_progress", - "translation": "В настоящее время у вас нет запусков." - }, - { - "id": "app.user.digest.runs_in_progress.num_in_progress", - "translation": { - "few": "У вас запущено {{.Count}}:", - "many": "У вас запущены {{.Count}}:", - "one": "У вас запущен {{.Count}}:" - } - }, - { - "id": "app.user.digest.runs_in_progress.heading", - "translation": "Выполняется" - }, - { - "id": "app.user.digest.overdue_status_updates.zero_overdue", - "translation": "У вас просрочено 0 запусков." - }, - { - "id": "app.user.digest.overdue_status_updates.num_overdue", - "translation": { - "few": "У вас есть {{.Count}} просроченных запуска для обновления статуса:", - "many": "У вас есть {{.Count}} просроченных запусков для обновления статуса:", - "one": "У вас есть {{.Count}} просроченный запуск для обновления статуса:" - } - }, - { - "id": "app.user.digest.overdue_status_updates.heading", - "translation": "Просроченные обновления статуса" - }, - { - "id": "app.command.execute.error", - "translation": "Невозможно выполнить команду." - }, { "id": "api.server.cws.subscribe_to_newsletter.app_error", "translation": "Серверу CWS не удалось подписаться на рассылку новостей." @@ -9883,258 +9683,14 @@ "id": "api.license.request-trial.bad-request.business-email", "translation": "Недействительный рабочий адрес электронной почты для пробной версии" }, - { - "id": "app.user.digest.tasks.due_in_x_days", - "translation": { - "few": "Срок исполнения - {{.Count}} дня", - "many": "Срок исполнения - {{.Count}} дней", - "one": "Срок исполнения - {{.Count}} день" - } - }, { "id": "app.plugin.skip_installation.app_error", "translation": "Пропуск установки плагина {{.Id}}, поскольку существующая версия плагина идентична текущей или более новая." }, - { - "id": "app.plugin.blocked.app_error", - "translation": "Плагин {{.Id}} находится в списке блокировки. Некоторые плагины заблокированы, потому что они встроены в данную версию Mattermost." - }, - { - "id": "app.user.run.confirm_finish.title", - "translation": "Подтвердить завершение запуска" - }, - { - "id": "app.user.run.confirm_finish.submit_label", - "translation": "Завершить запуск" - }, - { - "id": "app.user.run.confirm_finish.num_outstanding", - "translation": { - "few": "Есть **{{.Count}} невыполненные задачи**. Вы уверены, что хотите завершить запуск *{{.RunName}}* для всех участников?", - "many": "Есть **{{.Count}} невыполненных задач**. Вы уверены, что хотите завершить запуск *{{.RunName}}* для всех участников?", - "one": "Есть **{{.Count}} невыполненная задача**. Вы уверены, что хотите завершить запуск *{{.RunName}}* для всех участников?" - } - }, - { - "id": "app.user.run.add_to_timeline.title", - "translation": "Добавить на временную шкалу" - }, - { - "id": "app.user.run.add_to_timeline.summary", - "translation": "Сводка" - }, - { - "id": "app.user.run.add_to_timeline.summary.placeholder", - "translation": "Краткая сводка, отображаемая на временной шкале" - }, - { - "id": "app.user.run.add_to_timeline.summary.help", - "translation": "Максимум 64 символа" - }, - { - "id": "app.user.run.add_to_timeline.submit_label", - "translation": "Добавить на временную шкалу" - }, - { - "id": "app.user.run.add_to_timeline.playbook_run", - "translation": "Запуск сценария" - }, - { - "id": "app.user.run.add_checklist_item.title", - "translation": "Добавить новую задачу" - }, - { - "id": "app.user.run.add_checklist_item.submit_label", - "translation": "Добавить задачу" - }, - { - "id": "app.user.run.add_checklist_item.name", - "translation": "Имя" - }, - { - "id": "app.user.run.add_checklist_item.description", - "translation": "Описание" - }, - { - "id": "app.user.new_run.title", - "translation": "Запустить сценарий" - }, - { - "id": "app.user.new_run.submit_label", - "translation": "Запустить" - }, - { - "id": "app.user.new_run.run_name", - "translation": "Название запуска" - }, - { - "id": "app.user.new_run.playbook", - "translation": "Сценарий" - }, - { - "id": "app.user.new_run.intro", - "translation": "**Владелец** {{.Username}}" - }, - { - "id": "app.user.digest.tasks.num_assigned", - "translation": { - "few": "Вам поручено {{.Count}} задачи:", - "many": "Вам поручено {{.Count}} задач:", - "one": "Вам поручена {{.Count}} задача:" - } - }, - { - "id": "app.user.digest.tasks.num_assigned_due_until_today", - "translation": { - "few": "Вы получили {{.Count}} задачи, срок выполнения которых:", - "many": "Вы получили {{.Count}} задач, срок выполнения которых:", - "one": "Вы получили {{.Count}} задачу, срок выполнения которой:" - } - }, - { - "id": "app.user.digest.tasks.zero_assigned", - "translation": "У вас нет назначенных задач." - }, - { - "id": "app.user.digest.tasks.heading", - "translation": "Ваши назначенные задачи" - }, - { - "id": "app.user.digest.tasks.due_yesterday", - "translation": "Вчера" - }, - { - "id": "app.user.digest.tasks.due_x_days_ago", - "translation": "Срок исполнения {{.Count}} дн. назад" - }, - { - "id": "app.user.digest.tasks.due_today", - "translation": "Сегодня" - }, - { - "id": "app.user.run.status_enable", - "translation": "@{{.Username}} включил обновление статуса для [{{.RunName}}]({{.RunURL}})" - }, - { - "id": "app.user.run.status_disable", - "translation": "@{{.Username}} отключил обновление статуса для [{{.RunName}}]({{.RunURL}})" - }, - { - "id": "app.user.run.request_update", - "translation": "@here — @{{.Name}} запросил обновление статуса для [{{.RunName}}]({{.RunURL}}). \n" - }, - { - "id": "app.user.run.request_join_channel", - "translation": "@{{.Name}} является участником запуска и хочет присоединиться к этому каналу. Любой участник канала может пригласить его.\n" - }, - { - "id": "worktemplate.product_teams.product_roadmap.channel", - "translation": "Обсудите со своей командой отзывы клиентов, расставьте приоритеты и вместе согласуйте прогресс и планы." - }, - { - "id": "worktemplate.product_teams.product_roadmap.board", - "translation": "Используйте доску \"Дорожная карта продукта\" (Product Roadmap) для управления отзывами пользователей, назначения ресурсов, просмотра результатов в календарном режиме и определения приоритетности проблем." - }, - { - "id": "worktemplate.product_teams.goals_and_okrs.integration", - "translation": "Повысьте производительность вашего канала, интегрировав наиболее часто используемые инструменты, такие как Zoom, для облегчения совместной работы. Они будут загружены для вас." - }, - { - "id": "worktemplate.product_teams.goals_and_okrs.channel", - "translation": "Общайтесь о своих целях и прогрессе работ с командой в режиме асинхронного или реального времени и будьте в курсе всех изменений в едином канале." - }, - { - "id": "worktemplate.product_teams.goals_and_okrs.board", - "translation": "Отслеживайте прогресс вашей команды в достижении организационных целей с помощью доски \"Цели и ОКР\" (OKR board). Следите за ходом совещаний с помощью доски \"Повестка совещания\" (Meeting Agenda)." - }, - { - "id": "worktemplate.product_teams.feature_release.description.playbook", - "translation": "Усильте межфункциональное сотрудничество команды с помощью контрольных списков задач и автоматизации, которые поддерживают процесс разработки функций. После завершения работы проведите ретроспективу и внесите улучшения в следующий релиз." - }, - { - "id": "worktemplate.product_teams.feature_release.description.integration", - "translation": "Повысьте производительность вашего канала, интегрировав наиболее часто используемые инструменты для поддержки выпуска функций, например GitHub. Они будут загружены для вас." - }, - { - "id": "worktemplate.product_teams.feature_release.description.channel", - "translation": "Общайтесь со своей командой о любых блокировках релиза и изменениях в канале, который легко подключается к вашим доскам, плейбукам и другим интеграциям." - }, - { - "id": "worktemplate.product_teams.feature_release.description.board", - "translation": "Ведите совещания по плану с помощью доски \"Повестка совещания\" (Meeting Agenda). Управляйте своей рабочей нагрузкой с помощью доски \"Задачи проекта\" (Project Tasks)." - }, - { - "id": "worktemplate.product_teams.bug_bash.playbook", - "translation": "Используйте контрольные списки для распределения областей тестирования и автоматизации задач для проведения комплексного процесса устранения ошибок. Используйте ретроспективу, чтобы проанализировать свой процесс и улучшить его для следующего раза." - }, - { - "id": "worktemplate.product_teams.bug_bash.integration", - "translation": "Повысьте продуктивность вашего канала, интегрировав наиболее часто используемые инструменты, такие как Jira, для отслеживания хода работы над ошибками. Они будут загружены для вас." - }, - { - "id": "worktemplate.product_teams.bug_bash.channel", - "translation": "Планируйте и управляйте сообщениями об ошибках и решениями в едином канале, который легко доступен для вашей команды и организации." - }, - { - "id": "worktemplate.leadership.goals_and_okrs.integration", - "translation": "Повысьте производительность вашего канала, интегрировав наиболее часто используемые инструменты, такие как Zoom, для облегчения совместной работы. Они будут загружены для вас." - }, - { - "id": "worktemplate.leadership.goals_and_okrs.channel", - "translation": "Общайтесь о своих целях и прогрессе работ с командой в режиме асинхронного или реального времени и будьте в курсе всех изменений в едином канале." - }, - { - "id": "worktemplate.leadership.goals_and_okrs.board", - "translation": "Отслеживайте прогресс вашей команды в достижении организационных целей с помощью доски \"Цели и ОКР\" (OKR board). Следите за ходом совещаний с помощью доски \"Повестка совещания\" (Meeting Agenda)." - }, - { - "id": "worktemplate.devops.product_release.playbook", - "translation": "Создайте повторяющиеся рабочие процессы, которые легко соблюдать и внедрять, чтобы выпуск продукции был надежным и своевременным." - }, - { - "id": "worktemplate.devops.product_release.channel", - "translation": "Легко и быстро общайтесь со своей командой по поводу ежедневных этапов, любых блокирующих факторов и изменений в результатах работы." - }, - { - "id": "worktemplate.devops.product_release.board", - "translation": "Используйте доску релиза продукта (Product Release) для поддержки сроков и процесса выпуска, чтобы каждый знал, какие задачи подлежат выполнению." - }, { "id": "worktemplate.devops.incident_resolution.description.playbook", "translation": "Используйте контрольные списки и автоматизацию, чтобы привлечь ключевых членов команды, и расскажите, как продвигается процесс разрешения инцидента." }, - { - "id": "worktemplate.devops.incident_resolution.description.channel", - "translation": "Общайтесь со своей командой о приоритетах, добавляйте заинтересованные стороны, предоставляйте обновления и работайте над решением в едином канале." - }, - { - "id": "worktemplate.devops.incident_resolution.description.board", - "translation": "Используйте доску по разрешению инцидентов для поддержки повторяющихся процессов и назначения определенных задач для всей команды." - }, - { - "id": "worktemplate.companywide.goals_and_okrs.integration", - "translation": "Повысьте производительность вашего канала, интегрировав наиболее часто используемые инструменты, такие как Zoom, для облегчения совместной работы. Они будут загружены для вас." - }, - { - "id": "worktemplate.companywide.goals_and_okrs.channel", - "translation": "Общайтесь о своих целях и прогрессе работ с командой в режиме асинхронного или реального времени и будьте в курсе всех изменений в едином канале." - }, - { - "id": "worktemplate.companywide.goals_and_okrs.board", - "translation": "Отслеживайте прогресс вашей команды в достижении организационных целей с помощью доски \"Цели и ОКР\". Следите за ходом совещаний с помощью доски \"Повестка совещания\"." - }, - { - "id": "worktemplate.companywide.create_project.integration", - "translation": "Повысьте производительность вашего канала, интегрировав наиболее часто используемые инструменты. Они будут загружены для вас." - }, - { - "id": "worktemplate.companywide.create_project.channel", - "translation": "Обсудите со своей командой новый проект и решите, как вы собираетесь его структурировать, в канале совместной работы." - }, - { - "id": "worktemplate.companywide.create_project.board", - "translation": "Используйте доску Kanban для определения и отслеживания задач и хода выполнения проекта." - }, { "id": "model.license_record.is_valid.bytes.app_error", "translation": "Недопустимое значение для байтов при загрузке лицензии." @@ -10144,47 +9700,179 @@ "translation": "Версия Elasticsearch {{.Version}} выше максимально поддерживаемой версии {{.MaxVersion}}" }, { - "id": "app.user.run.update_status.title", - "translation": "Статус обновлен" + "id": "api.user.invalidate_password_recovery_tokens.error", + "translation": "Невозможно получить токены по типу при аннулировании токенов восстановления пароля" }, { - "id": "app.user.run.update_status.submit_label", - "translation": "Обновить статус" + "id": "api.user.invalidate_password_recovery_tokens_parse.error", + "translation": "Невозможно разобрать токен при аннулировании токенов восстановления пароля" }, { - "id": "app.user.run.update_status.reminder_for_next_update", - "translation": "Напоминание для следующего обновления" + "id": "api.post.post_priority.urgent_persistent_notification_post.request_error", + "translation": "Сообщения с постоянными уведомлениями должны иметь приоритет \"Срочно\"." }, { - "id": "app.user.run.update_status.num_channel", - "translation": { - "few": "Предоставьте заинтересованным сторонам обновленную информацию. Этот пост будет транслироваться на {{.Count}} канала.", - "many": "Предоставьте заинтересованным сторонам обновленную информацию. Этот пост будет транслироваться на {{.Count}} каналов.", - "one": "Предоставьте заинтересованным сторонам обновленную информацию. Этот пост будет транслироваться на {{.Count}} канал." - } + "id": "api.post.post_priority.max_recipients_persistent_notification_post.request_error", + "translation": "Сообщения постоянных уведомлений допускают максимум {{.MaxRecipients}} получателей." }, { - "id": "app.user.run.update_status.finish_run.placeholder", - "translation": "Отметьте запуск как завершенный" + "id": "api.post.post_priority.min_recipients_persistent_notification_post.request_error", + "translation": "Сообщения постоянных уведомлений должны иметь минимум 1 получателя." }, { - "id": "app.user.run.update_status.finish_run", - "translation": "Завершить запуск" + "id": "api.post.post_priority.persistent_notification_validation_error.request_error", + "translation": "Проверка постоянного уведомления не прошла." }, { - "id": "app.user.run.update_status.change_since_last_update", - "translation": "Изменения с последнего обновления" + "id": "api.license.true_up_review.failed_to_submit", + "translation": "Не удалось отправить точный профиль проверки в CWS." }, { - "id": "worktemplate.product_teams.sprint_planning.integration", - "translation": "Повысьте производительность вашего канала за счет интеграции наиболее часто используемых инструментов, таких как Zoom. Они будут загружены для вас." + "id": "api.post.post_priority.priority_post_not_allowed_for_user.request_error", + "translation": "Пользователю не разрешается создавать приоритетные сообщения или постоянные уведомления." }, { - "id": "worktemplate.product_teams.sprint_planning.channel", - "translation": "Общайтесь со своей командой в канале, который легко подключается к вашим доскам и интеграциям." + "id": "api.command_exportlink.desc", + "translation": "Сгенерировать ссылку для загрузки экспорта." }, { - "id": "worktemplate.product_teams.sprint_planning.board", - "translation": "Отслеживайте прогресс вашей команды в достижении еженедельных целей с помощью разбивки спринтов, определения приоритетов, назначения владельцев и комментариев." + "id": "api.command_exportlink.driver.app_error", + "translation": "Драйвер файлового хранилища не поддерживает генерацию ссылок." + }, + { + "id": "api.command_exportlink.empty.app_error", + "translation": "Не найден файл экспорта." + }, + { + "id": "api.command_exportlink.hint", + "translation": "[job-id|название файла zip|{{.LatestMsg}}]" + }, + { + "id": "api.command_exportlink.invalid.app_error", + "translation": "Невозможно найти запрошенный файл." + }, + { + "id": "api.command_exportlink.link.text", + "translation": "Загрузить файл можно здесь: {{.Link}}.\nСрок действия этой ссылки истекает через {{.Expiration}}." + }, + { + "id": "api.command_exportlink.list.app_error", + "translation": "Невозможно получить список экспорта." + }, + { + "id": "api.command_exportlink.name", + "translation": "exportlink" + }, + { + "id": "api.command_exportlink.permission.app_error", + "translation": "У вас недостаточно прав для выполнения этой команды." + }, + { + "id": "api.command_exportlink.presign.app_error", + "translation": "Невозможно сгенерировать предварительный url." + }, + { + "id": "app.channel.get_channels_member_count.existing.app_error", + "translation": "Невозможно найти количество участников для заданных каналов." + }, + { + "id": "app.channel.get_channels_member_count.find.app_error", + "translation": "Невозможно найти количество участников." + }, + { + "id": "app.channel.update_member.app_error", + "translation": "Невозможно обновить участника канала." + }, + { + "id": "app.eport.generate_presigned_url.config.app_error", + "translation": "Эти действия требуют использования специализированного экспортного магазина." + }, + { + "id": "app.eport.generate_presigned_url.driver.app_error", + "translation": "Драйвер вашего экспортного хранилища не поддерживает генерацию url-значений." + }, + { + "id": "api.error_no_organization_name_provided_for_self_hosted_onboarding", + "translation": "Ошибка: не указано название организации для самостоятельной регистрации на сайте." + }, + { + "id": "api.user.invalidate_password_recovery_tokens_delete.error", + "translation": "Невозможно удалить маркер при аннулировании маркеров восстановления пароля" + }, + { + "id": "api.post.post_priority.priority_post_only_allowed_for_root_post.request_error", + "translation": "Приоритет имеют только корневые посты." + }, + { + "id": "app.post_persistent_notification.delete_by_team.app_error", + "translation": "Невозможно удалить постоянные уведомления по команде." + }, + { + "id": "app.post_persistent_notification.delete_by_channel.app_error", + "translation": "Невозможно удалить постоянные уведомления по каналу." + }, + { + "id": "app.post.delete_post.get_team.app_error", + "translation": "Произошла ошибка при получении Команды." + }, + { + "id": "model.config.is_valid.persistent_notifications_interval.app_error", + "translation": "Неверная частота постоянных уведомлений. Должна быть не менее двух минут." + }, + { + "id": "app.channel.update_member.notify_props_limit_exceeded.app_error", + "translation": "Не удалось обновить участника канала, превышено ограничения размера реквизита." + }, + { + "id": "app.eport.generate_presigned_url.featureflag.app_error", + "translation": "Эта функция ограничена флагом функции." + }, + { + "id": "app.eport.generate_presigned_url.fileexist.app_error", + "translation": "Не удалось проверить, существует ли файл." + }, + { + "id": "app.eport.generate_presigned_url.link.app_error", + "translation": "Невозможно сгенерировать заданный url." + }, + { + "id": "app.eport.generate_presigned_url.notfound.app_error", + "translation": "Файл экспорта не найден." + }, + { + "id": "app.file_info.get.gif.app_error", + "translation": "Не удалось декодировать gif." + }, + { + "id": "app.post_priority.delete_persistent_notification_post.app_error", + "translation": "Не удалось удалить постоянный пост уведомления" + }, + { + "id": "model.channel_member.is_valid.notify_props.app_error", + "translation": "Сообщить о превышении ограничения на размер реквизита." + }, + { + "id": "model.config.is_valid.elastic_search.ignored_indexes_dash_prefix.app_error", + "translation": "Игнорируемые индексы для очистки не должны начинаться с тире." + }, + { + "id": "model.config.is_valid.persistent_notifications_count.app_error", + "translation": "Неверное общее количество постоянных уведомлений на сообщение. Должно быть положительное число." + }, + { + "id": "model.config.is_valid.persistent_notifications_recipients.app_error", + "translation": "Неверное максимальное количество получателей для постоянных уведомлений. Должно быть положительное число." + }, + { + "id": "app.save_config.plugin_hook_error", + "translation": "Произошла ошибка при выполнении хука плагина при сохранении конфигурации." + }, + { + "id": "model.channel_member.is_valid.channel_auto_follow_threads_value.app_error", + "translation": "Неверное значение channel-auto-follow-threads." + }, + { + "id": "model.config.is_valid.local_mode_socket.app_error", + "translation": "Невозможно найти каталог локального файла сокета." } ] diff --git a/server/i18n/si.json b/server/i18n/si.json new file mode 100644 index 00000000000..66a09e462c4 --- /dev/null +++ b/server/i18n/si.json @@ -0,0 +1,50 @@ +[ + { + "id": "November", + "translation": "ඉල්" + }, + { + "id": "October", + "translation": "වප්" + }, + { + "id": "September", + "translation": "බිනර" + }, + { + "id": "December", + "translation": "උදුවප්" + }, + { + "id": "August", + "translation": "නිකිණි" + }, + { + "id": "July", + "translation": "ඇසළ" + }, + { + "id": "June", + "translation": "පොසොන්" + }, + { + "id": "May", + "translation": "වෙසක්" + }, + { + "id": "March", + "translation": "මැදින්" + }, + { + "id": "February", + "translation": "නවම්" + }, + { + "id": "January", + "translation": "දුරුතු" + }, + { + "id": "April", + "translation": "බක්" + } +] diff --git a/server/i18n/sl.json b/server/i18n/sl.json new file mode 100644 index 00000000000..16066791f2f --- /dev/null +++ b/server/i18n/sl.json @@ -0,0 +1,4698 @@ +[ + { + "id": "web.incoming_webhook.user.app_error", + "translation": "Uporabnika ni mogoče najti " + }, + { + "id": "web.incoming_webhook.text.app_error", + "translation": "Ni besedila" + }, + { + "id": "web.incoming_webhook.permissions.app_error", + "translation": "Neprimerna dovoljenja za kanale" + }, + { + "id": "web.incoming_webhook.parse.app_error", + "translation": "Dohodnih podatkov ni mogoče razbrati" + }, + { + "id": "web.incoming_webhook.invalid.app_error", + "translation": "Neveljavna metoda" + }, + { + "id": "web.incoming_webhook.disabled.app_error", + "translation": "Administrator je onemogočil vhodne spletne zanke." + }, + { + "id": "web.incoming_webhook.channel.app_error", + "translation": "Kanala ni bilo mogoče najti" + }, + { + "id": "web.get_access_token.internal_saving.app_error", + "translation": "Podatkov za dostop uporabnika ni mogoče posodobiti." + }, + { + "id": "web.command_webhook.parse.app_error", + "translation": "Dohodnih podatkov ni mogoče razbrati" + }, + { + "id": "web.command_webhook.command.app_error", + "translation": "Ukaza ni bilo mogoče najti" + }, + { + "id": "system.message.name", + "translation": "Sistem" + }, + { + "id": "app.user_terms_of_service.delete.app_error", + "translation": "Pogojev storitve ni mogoče izbrisati." + }, + { + "id": "store.sql_user.get_for_login.app_error", + "translation": "Ni mogoče najti obstoječega računa, ki se ujema z vašimi poverilnicami. Ta ekipa lahko potrebuje vabilo lastnika ekipe za pridružitev." + }, + { + "id": "store.sql_team.save_member.exists.app_error", + "translation": "Član ekipe s tem ID-jem že obstaja" + }, + { + "id": "app.emoji.delete.no_results", + "translation": "Ne moremo najti čustvenega simbola za brisanje" + }, + { + "id": "app.emoji.delete.app_error", + "translation": "Čustvenih simbolov ni mogoče izbrisati" + }, + { + "id": "store.sql_command.save.get.app_error", + "translation": "Ukaza ni mogoče dobiti" + }, + { + "id": "app.channel_member_history.log_leave_event.internal_error", + "translation": "Zgodovina članov kanala ni bila posneta. Posodobitev obstoječega posnetka pridružitve ni uspela" + }, + { + "id": "app.channel_member_history.log_join_event.internal_error", + "translation": "Zgodovina članov kanala ni bila posneta" + }, + { + "id": "app.channel.update.bad_id", + "translation": "Kanala ni mogoče posodobiti" + }, + { + "id": "store.sql_channel.save_channel.limit.app_error", + "translation": "Dosegli ste omejitev števila dovoljenih kanalov." + }, + { + "id": "store.sql_channel.save_channel.exists.app_error", + "translation": "Kanal s tem imenom že obstaja v isti skupini" + }, + { + "id": "store.sql_channel.save_channel.existing.app_error", + "translation": "Za obstoječi kanal se mora izvesti posodobitev" + }, + { + "id": "store.sql_channel.save.direct_channel.app_error", + "translation": "Uporabite SaveDirectChannel za ustvarjanje neposrednega kanala" + }, + { + "id": "store.sql_channel.save.archived_channel.app_error", + "translation": "Arhiviranega kanala ne morete spremeniti" + }, + { + "id": "app.channel.get_deleted.missing.app_error", + "translation": "Izbrisani kanali ne obstajajo" + }, + { + "id": "app.channel.get_deleted.existing.app_error", + "translation": "Obstoječega izbrisanega kanala ni mogoče najti" + }, + { + "id": "app.channel.get_channels.not_found.app_error", + "translation": "Ni bilo najdenih kanalov" + }, + { + "id": "app.channel.get_by_name.missing.app_error", + "translation": "Kanal ne obstaja" + }, + { + "id": "store.sql_channel.get.existing.app_error", + "translation": "Ni mogoče najti obstoječega kanala" + }, + { + "id": "app.audit.save.saving.app_error", + "translation": "Prišlo je do napake pri shranjevanju revizije" + }, + { + "id": "app.audit.permanent_delete_by_user.app_error", + "translation": "Prišlo je do napake pri brisanju revizij" + }, + { + "id": "app.audit.get.finding.app_error", + "translation": "Pri iskanju revizij smo naleteli na napako" + }, + { + "id": "plugin_api.send_mail.missing_to", + "translation": "Manjkajoči ZA naslov." + }, + { + "id": "plugin_api.send_mail.missing_subject", + "translation": "Manjka predmet e-pošte." + }, + { + "id": "plugin_api.get_file_link.no_post.app_error", + "translation": "Ni mogoče dobiti javne povezave za datoteko. Datoteka mora biti dodana k objavi, ki jo trenutno uporabnik lahko bere." + }, + { + "id": "plugin_api.get_file_link.disabled.app_error", + "translation": "Javne povezave so onemogočene" + }, + { + "id": "plugin.api.update_user_status.bad_status", + "translation": "Statusa uporabnika ni mogoče nastaviti. Neznan status uporabnika." + }, + { + "id": "oauth.gitlab.tos.error", + "translation": "Pogoji storitve GitLab so posodobljeni. Prosimo, pojdite na gitlab.com, da jih sprejmete in se nato poskusite znova prijaviti v Mattermost." + }, + { + "id": "model.websocket_client.connect_fail.app_error", + "translation": "Ni mogoče vzpostaviti povezave s strežnikom WebSocket." + }, + { + "id": "model.utils.decode_json.app_error", + "translation": "ni bilo mogoče dekodirati" + }, + { + "id": "model.user_access_token.is_valid.user_id.app_error", + "translation": "Neveljaven ID uporabnika." + }, + { + "id": "model.user_access_token.is_valid.token.app_error", + "translation": "Neveljaven žeton za dostop" + }, + { + "id": "model.user_access_token.is_valid.id.app_error", + "translation": "Neveljavna ID vrednost." + }, + { + "id": "model.user_access_token.is_valid.description.app_error", + "translation": "Neveljaven opis, mora imeti 255 ali manj znakov" + }, + { + "id": "model.user.is_valid.username.app_error", + "translation": "Uporabniško ime se mora začeti s črko in mora vsebovati od 3 do 22 znakov, vključno s številkami, malimi črkami in simboli \".\", \"-\" in \"_\"." + }, + { + "id": "model.user.is_valid.update_at.app_error", + "translation": "Posodobi na mora biti veljaven čas" + }, + { + "id": "model.user.is_valid.pwd_uppercase_symbol.app_error", + "translation": "Vaše geslo mora vsebovati vsaj {{.Min}} znakov, sestavljenih iz vsaj ene velike črke in vsaj enega simbola (npr. \"~! @ # $% ^ & * ()\")." + }, + { + "id": "model.user.is_valid.pwd_uppercase_number_symbol.app_error", + "translation": "Vaše geslo mora vsebovati vsaj {{.Min}} znakov, sestavljenih iz vsaj ene velike črke, vsaj ene številke in vsaj enega simbola (npr. \"~! @ # $% ^ & * ()\")." + }, + { + "id": "model.user.is_valid.pwd_uppercase_number.app_error", + "translation": "Vaše geslo mora vsebovati vsaj {{.Min}} znake, sestavljene iz vsaj ene velike črke in vsaj ene številke." + }, + { + "id": "model.user.is_valid.pwd_uppercase.app_error", + "translation": "Vaše geslo mora vsebovati vsaj {{.Min}} znake, sestavljene iz vsaj ene velike črke." + }, + { + "id": "model.user.is_valid.pwd_symbol.app_error", + "translation": "Vaše geslo mora vsebovati vsaj {{.Min}} znakov, sestavljenih iz vsaj enega simbola (npr. \"~! @ # $% ^ & * ()\")." + }, + { + "id": "model.user.is_valid.pwd_number_symbol.app_error", + "translation": "Vaše geslo mora vsebovati vsaj {{.Min}} znakov, sestavljenih iz vsaj ene številke in vsaj enega simbola (npr. \"~! @ # $% ^ & * ()\")." + }, + { + "id": "model.user.is_valid.pwd_number.app_error", + "translation": "Vaše geslo mora vsebovati vsaj {{.Min}} znakov, sestavljenih iz vsaj ene številke." + }, + { + "id": "model.user.is_valid.pwd_lowercase_uppercase_symbol.app_error", + "translation": "Vaše geslo mora vsebovati vsaj {{.Min}} znake, sestavljene iz vsaj ene male črke, vsaj ene velike črke in vsaj enega simbola (npr. \"~! @ # $% ^ & * ()\")." + }, + { + "id": "model.user.is_valid.pwd_lowercase_uppercase_number_symbol.app_error", + "translation": "Vaše geslo mora vsebovati vsaj {{.Min}} znakov, sestavljenih iz vsaj ene male črke, vsaj ene velike črke, vsaj ene številke in vsaj enega simbola (npr.: ~! @ # $% ^ & * () \")." + }, + { + "id": "model.user.is_valid.pwd_lowercase_uppercase_number.app_error", + "translation": "Your password must contain at least {{.Min}} characters made up of at least one lowercase letter, at least one uppercase letter, and at least one number." + }, + { + "id": "model.user.is_valid.pwd_lowercase_uppercase.app_error", + "translation": "Vaše geslo mora vsebovati vsaj {{.Min}} znake, sestavljene iz vsaj ene male črke in vsaj ene velike črke." + }, + { + "id": "model.user.is_valid.pwd_lowercase_symbol.app_error", + "translation": "Vaše geslo mora vsebovati vsaj {{.Min}} znakov, sestavljenih iz vsaj ene male črke in vsaj enega simbola (npr. \"~! @ # $% ^ & * ()\")." + }, + { + "id": "model.user.is_valid.pwd_lowercase_number_symbol.app_error", + "translation": "Vaše geslo mora vsebovati vsaj {{.Min}} znakov, sestavljenih iz vsaj ene male črke, vsaj ene številke in vsaj enega simbola (npr.: \"~! @ # $% ^ & * ()\")." + }, + { + "id": "model.user.is_valid.pwd_lowercase_number.app_error", + "translation": "Vaše geslo mora vsebovati vsaj {{.Min}} znake, sestavljene iz vsaj ene male črke in vsaj ene številke." + }, + { + "id": "model.user.is_valid.pwd_lowercase.app_error", + "translation": "Vaše geslo mora vsebovati vsaj {{.Min}} znakov, sestavljenih iz vsaj ene male črke." + }, + { + "id": "model.user.is_valid.pwd.app_error", + "translation": "Vaše geslo mora vsebovati vsaj {{.Min}} znakov." + }, + { + "id": "model.user.is_valid.nickname.app_error", + "translation": "Neveljavno uporabniško ime." + }, + { + "id": "model.user.is_valid.locale.app_error", + "translation": "Neveljavno uporabniško ime." + }, + { + "id": "model.user.is_valid.last_name.app_error", + "translation": "Neveljavno uporabniško ime." + }, + { + "id": "model.user.is_valid.id.app_error", + "translation": "Neveljaven ID uporabnika." + }, + { + "id": "model.user.is_valid.first_name.app_error", + "translation": "Neveljavno uporabniško ime." + }, + { + "id": "model.user.is_valid.email.app_error", + "translation": "Neveljavna e-pošta" + }, + { + "id": "model.user.is_valid.create_at.app_error", + "translation": "Ustvari na mora biti veljaven čas" + }, + { + "id": "model.token.is_valid.size", + "translation": "Neveljaven žeton." + }, + { + "id": "model.token.is_valid.expiry", + "translation": "Neveljaven potek veljavnosti žetona" + }, + { + "id": "model.team_member.is_valid.user_id.app_error", + "translation": "Neveljaven ID uporabnika." + }, + { + "id": "model.team_member.is_valid.team_id.app_error", + "translation": "Neveljaven ID ekipe" + }, + { + "id": "model.team.is_valid.url.app_error", + "translation": "Neveljaven URL identifikator" + }, + { + "id": "model.team.is_valid.update_at.app_error", + "translation": "Posodobi na mora biti veljaven čas" + }, + { + "id": "model.team.is_valid.type.app_error", + "translation": "Neveljavna vrsta" + }, + { + "id": "model.team.is_valid.reserved.app_error", + "translation": "Ta URL ni na voljo. Poskusite drugega." + }, + { + "id": "model.team.is_valid.name.app_error", + "translation": "Neveljavno ime" + }, + { + "id": "model.team.is_valid.id.app_error", + "translation": "Neveljaven ID" + }, + { + "id": "model.team.is_valid.email.app_error", + "translation": "Neveljavna e-pošta" + }, + { + "id": "model.team.is_valid.domains.app_error", + "translation": "Neveljavne dovoljene domene" + }, + { + "id": "model.team.is_valid.description.app_error", + "translation": "Neveljaven opis" + }, + { + "id": "model.team.is_valid.create_at.app_error", + "translation": "Ustvari na mora biti veljaven čas" + }, + { + "id": "model.team.is_valid.company.app_error", + "translation": "Neveljavno ime podjetja" + }, + { + "id": "model.team.is_valid.characters.app_error", + "translation": "Ime mora vsebovati 2 ali več malih alfanumeričnih znakov" + }, + { + "id": "model.reaction.is_valid.user_id.app_error", + "translation": "Neveljaven ID uporabnika." + }, + { + "id": "model.reaction.is_valid.post_id.app_error", + "translation": "Neveljaven ID objave" + }, + { + "id": "model.reaction.is_valid.emoji_name.app_error", + "translation": "Neveljavno ime čustvenega simbola" + }, + { + "id": "model.reaction.is_valid.create_at.app_error", + "translation": "Ustvari na mora biti veljaven čas" + }, + { + "id": "model.preference.is_valid.value.app_error", + "translation": "Vrednost je predolga" + }, + { + "id": "model.preference.is_valid.theme.app_error", + "translation": "Neveljavna tema" + }, + { + "id": "model.preference.is_valid.name.app_error", + "translation": "Neveljavno ime" + }, + { + "id": "model.preference.is_valid.id.app_error", + "translation": "Neveljaven ID uporabnika." + }, + { + "id": "model.preference.is_valid.category.app_error", + "translation": "Neveljavna kategorija" + }, + { + "id": "model.post.is_valid.user_id.app_error", + "translation": "Neveljaven ID uporabnika." + }, + { + "id": "model.post.is_valid.update_at.app_error", + "translation": "Posodobi na mora biti veljaven čas" + }, + { + "id": "model.post.is_valid.type.app_error", + "translation": "Neveljavna vrsta" + }, + { + "id": "model.post.is_valid.root_id.app_error", + "translation": "Neveljaven root ID" + }, + { + "id": "model.post.is_valid.props.app_error", + "translation": "Neveljavni rekviziti" + }, + { + "id": "model.post.is_valid.original_id.app_error", + "translation": "Neveljaven original ID" + }, + { + "id": "model.post.is_valid.msg.app_error", + "translation": "Neveljavno sporočilo" + }, + { + "id": "model.post.is_valid.id.app_error", + "translation": "Neveljaven ID" + }, + { + "id": "model.post.is_valid.filenames.app_error", + "translation": "Neveljavna imena datotek" + }, + { + "id": "model.post.is_valid.file_ids.app_error", + "translation": "Neveljavni ID-ji datotek. Upoštevajte, da so prenosi omejeni na največ 5 datotek. Za dodatne datoteke uporabite dodatne objave." + }, + { + "id": "model.post.is_valid.create_at.app_error", + "translation": "Ustvari na mora biti veljaven čas" + }, + { + "id": "model.post.is_valid.channel_id.app_error", + "translation": "Neveljaven ID kanala" + }, + { + "id": "model.plugin_key_value.is_valid.plugin_id.app_error", + "translation": "Neveljaven ID vtičnika, mora vsebovati več kot {{.Min}} in največ {{.Max}} znakov." + }, + { + "id": "model.plugin_key_value.is_valid.key.app_error", + "translation": "Neveljaven ključ, mora vsebovati več kot {{.Min}} in največ {{.Max}} znakov." + }, + { + "id": "model.plugin_command.error.app_error", + "translation": "Pri poskusu izvajanja tega ukaza je prišlo do napake." + }, + { + "id": "model.outgoing_hook.username.app_error", + "translation": "Neveljavno uporabniško ime." + }, + { + "id": "model.outgoing_hook.is_valid.user_id.app_error", + "translation": "Neveljaven ID uporabnika." + }, + { + "id": "model.outgoing_hook.is_valid.url.app_error", + "translation": "Neveljavni URLji. Morajo biti veljavni URLji in se začeti s http:// ali https://" + }, + { + "id": "model.outgoing_hook.is_valid.update_at.app_error", + "translation": "Posodobi na mora biti veljaven čas" + }, + { + "id": "model.outgoing_hook.is_valid.token.app_error", + "translation": "Neveljaven žeton." + }, + { + "id": "model.outgoing_hook.is_valid.team_id.app_error", + "translation": "Neveljaven ID ekipe" + }, + { + "id": "model.outgoing_hook.is_valid.id.app_error", + "translation": "Neveljaven ID" + }, + { + "id": "model.outgoing_hook.is_valid.display_name.app_error", + "translation": "Neveljaven naslov" + }, + { + "id": "model.outgoing_hook.is_valid.description.app_error", + "translation": "Neveljaven opis" + }, + { + "id": "model.outgoing_hook.is_valid.create_at.app_error", + "translation": "Ustvari na mora biti veljaven čas" + }, + { + "id": "model.outgoing_hook.is_valid.channel_id.app_error", + "translation": "Neveljaven ID kanala" + }, + { + "id": "model.outgoing_hook.icon_url.app_error", + "translation": "Neveljavna ikona" + }, + { + "id": "model.oauth.is_valid.update_at.app_error", + "translation": "Posodobi na mora biti veljaven čas" + }, + { + "id": "model.oauth.is_valid.name.app_error", + "translation": "Neveljavno ime" + }, + { + "id": "model.oauth.is_valid.icon_url.app_error", + "translation": "URL za ikone mora biti veljaven URL in se začeti s http:// ali https://." + }, + { + "id": "model.oauth.is_valid.homepage.app_error", + "translation": "Domača stran mora imeti veljaven URL in se začeti s http:// ali https://." + }, + { + "id": "model.oauth.is_valid.description.app_error", + "translation": "Neveljaven opis" + }, + { + "id": "model.oauth.is_valid.creator_id.app_error", + "translation": "Neveljaven ID ustvarjalca" + }, + { + "id": "model.oauth.is_valid.create_at.app_error", + "translation": "Ustvari na mora biti veljaven čas" + }, + { + "id": "model.oauth.is_valid.client_secret.app_error", + "translation": "Neveljavna skrivnost odjemalca" + }, + { + "id": "model.oauth.is_valid.callback.app_error", + "translation": "Neveljaven URL. Mora biti veljaven URL in se začeti s http:// ali https://" + }, + { + "id": "model.oauth.is_valid.app_id.app_error", + "translation": "Neveljaven ID aplikacije" + }, + { + "id": "model.link_metadata.is_valid.url.app_error", + "translation": "URL metapodatkov povezave mora biti nastavljen" + }, + { + "id": "model.link_metadata.is_valid.type.app_error", + "translation": "Neveljaven tip metapodatkov povezave" + }, + { + "id": "model.link_metadata.is_valid.timestamp.app_error", + "translation": "Časovni žig metapodatkov povezave mora biti različen od nič in zaokrožen na najbližjo uro" + }, + { + "id": "model.link_metadata.is_valid.data_type.app_error", + "translation": "Podatki metapodatkov povezave se ne ujemajo s podanim tipom" + }, + { + "id": "model.link_metadata.is_valid.data.app_error", + "translation": "Podatki o metapodatkih povezav ne morejo biti ničelni" + }, + { + "id": "model.license_record.is_valid.id.app_error", + "translation": "Neveljavna vrednost za ID pri prenosu licence." + }, + { + "id": "model.license_record.is_valid.create_at.app_error", + "translation": "Neveljavna vrednost za create_at pri prenosu licence." + }, + { + "id": "model.job.is_valid.type.app_error", + "translation": "Neveljaven tip opravila" + }, + { + "id": "model.job.is_valid.status.app_error", + "translation": "Neveljavno stanje opravila" + }, + { + "id": "model.job.is_valid.id.app_error", + "translation": "Neveljaven ID opravila" + }, + { + "id": "model.job.is_valid.create_at.app_error", + "translation": "Ustvari na mora biti veljaven čas" + }, + { + "id": "model.incoming_hook.username.app_error", + "translation": "Neveljavno uporabniško ime." + }, + { + "id": "model.incoming_hook.user_id.app_error", + "translation": "Neveljaven ID uporabnika." + }, + { + "id": "model.incoming_hook.update_at.app_error", + "translation": "Posodobi na mora biti veljaven čas" + }, + { + "id": "model.incoming_hook.team_id.app_error", + "translation": "Neveljaven ID ekipe" + }, + { + "id": "model.incoming_hook.parse_data.app_error", + "translation": "Dohodnih podatkov ni mogoče razbrati" + }, + { + "id": "model.incoming_hook.id.app_error", + "translation": "Neveljaven ID" + }, + { + "id": "model.incoming_hook.icon_url.app_error", + "translation": "Neveljavna ikona objave" + }, + { + "id": "model.incoming_hook.display_name.app_error", + "translation": "Neveljaven naslov" + }, + { + "id": "model.incoming_hook.description.app_error", + "translation": "Neveljaven opis" + }, + { + "id": "model.incoming_hook.create_at.app_error", + "translation": "Ustvari na mora biti veljaven čas" + }, + { + "id": "model.incoming_hook.channel_id.app_error", + "translation": "Neveljaven ID kanala" + }, + { + "id": "model.guest.is_valid.emails.app_error", + "translation": "Neveljavna e-pošta" + }, + { + "id": "model.guest.is_valid.email.app_error", + "translation": "Neveljavna e-pošta" + }, + { + "id": "model.guest.is_valid.channels.app_error", + "translation": "Neveljaven ID kanala" + }, + { + "id": "model.guest.is_valid.channel.app_error", + "translation": "Neveljaven ID kanala" + }, + { + "id": "model.group_syncable.group_id.app_error", + "translation": "neveljavna lastnost ID skupine za člana skupine" + }, + { + "id": "model.group_member.user_id.app_error", + "translation": "neveljavna lastnost ID uporabnika za člana skupine" + }, + { + "id": "model.group_member.group_id.app_error", + "translation": "neveljavna lastnost ID skupine za člana skupine" + }, + { + "id": "model.group.update_at.app_error", + "translation": "neveljavna posodobitev za lastnost za skupino" + }, + { + "id": "migrations.worker.run_advanced_permissions_phase_2_migration.invalid_progress", + "translation": "Migracija ni uspela zaradi neveljavnih podatkov o napredku." + }, + { + "id": "mfa.validate_token.authenticate.app_error", + "translation": "Neveljaven MFA žeton." + }, + { + "id": "mfa.mfa_disabled.app_error", + "translation": "Več-faktorsko preverjanje pristnosti je bilo onemogočeno na tem strežniku." + }, + { + "id": "mfa.generate_qr_code.create_code.app_error", + "translation": "Napaka pri ustvarjanju kode QR" + }, + { + "id": "mfa.activate.bad_token.app_error", + "translation": "Neveljaven MFA žeton." + }, + { + "id": "mattermost.bulletin.subject", + "translation": "Mattermost varnostni bilten" + }, + { + "id": "manaultesting.test_autolink.unable.app_error", + "translation": "Ni mogoče najti kanalov" + }, + { + "id": "manaultesting.manual_test.parse.app_error", + "translation": "URL ni mogoče razčleniti" + }, + { + "id": "jobs.set_job_error.update.error", + "translation": "Stanja opravila ni bilo mogoče nastaviti na napako" + }, + { + "id": "jobs.request_cancellation.status.error", + "translation": "Ni možno zahtevati preklica za opravilo, ki ni v stanju, ki ga je mogoče preklicati." + }, + { + "id": "interactive_message.generate_trigger_id.signing_failed", + "translation": "Napaka pri podpisovanju trigger ID za interaktivni dialog." + }, + { + "id": "interactive_message.decode_trigger_id.verify_signature_failed", + "translation": "Preverjanje podpisa ni uspelo zaradi ID-ja sprožilca za interaktivno pogovorno okno." + }, + { + "id": "interactive_message.decode_trigger_id.signature_decode_failed", + "translation": "Dekodiranje base64 podpisa ID-ja sprožilca za interaktivno pogovorno okno ni uspelo." + }, + { + "id": "interactive_message.decode_trigger_id.missing_data", + "translation": "Manjkajo zahtevani podatki za Trigger ID interaktivnega dialoga." + }, + { + "id": "interactive_message.decode_trigger_id.expired", + "translation": "ID sprožilca za interaktivno pogovorno okno je potekel. ID-ji prožilcev so na voljo za največ {{.Seconds}} sekund." + }, + { + "id": "interactive_message.decode_trigger_id.base64_decode_failed_signature", + "translation": "Dekodiranje base64 podpisa ID-ja sprožilca za interaktivno pogovorno okno ni uspelo." + }, + { + "id": "interactive_message.decode_trigger_id.base64_decode_failed", + "translation": "Napaka pri dekodiranju base64 za trigger ID za interaktivni dialog." + }, + { + "id": "ent.saml.service_disable.app_error", + "translation": "SAML 2.0 ni podprt ali nastavljen na strežniku." + }, + { + "id": "ent.saml.metadata.app_error", + "translation": "Napaka pri izdelavi Service Provider Metadata." + }, + { + "id": "ent.saml.license_disable.app_error", + "translation": "Vaša licenca ne podpira overjanja SAML." + }, + { + "id": "ent.saml.do_login.validate.app_error", + "translation": "Pri preverjanju odgovora ponudnika identitete je prišlo do napake. Obrnite se na skrbnika sistema." + }, + { + "id": "ent.saml.do_login.parse.app_error", + "translation": "Med razčlenjevanjem odgovora ponudnika identitete je prišlo do napake. Obrnite se na skrbnika sistema." + }, + { + "id": "ent.saml.do_login.empty_response.app_error", + "translation": "Od ponudnika identitete smo prejeli prazen odgovor" + }, + { + "id": "ent.saml.do_login.decrypt.app_error", + "translation": "Prijava SAML ni bila uspešna, ker je prišlo do napake pri dešifriranju odgovora ponudnika identitete. Obrnite se na skrbnika sistema." + }, + { + "id": "ent.saml.configure.not_encrypted_response.app_error", + "translation": "Prijava SAML ni bila uspešna, ker odgovor ponudnika identitete ni šifriran. Obrnite se na skrbnika sistema." + }, + { + "id": "ent.saml.configure.load_private_key.app_error", + "translation": "Prijava SAML ni bila uspešna, ker zasebnega ključa ponudnika storitev ni bilo mogoče najti. Obrnite se na skrbnika sistema." + }, + { + "id": "ent.saml.configure.load_idp_cert.app_error", + "translation": "Datoteke za javno potrdilo ponudnika identitete ni bilo mogoče najti. Obrnite se na skrbnika sistema." + }, + { + "id": "ent.saml.configure.encryption_not_enabled.app_error", + "translation": "Prijava SAML ni bila uspešna, ker šifriranje ni omogočeno. Obrnite se na skrbnika sistema." + }, + { + "id": "ent.saml.build_request.encoding.app_error", + "translation": "Med kodiranjem zahteve za ponudnika identitete je prišlo do napake. Obrnite se na skrbnika sistema." + }, + { + "id": "ent.saml.build_request.app_error", + "translation": "Pri sprožitvi zahteve do ponudnika identitete je prišlo do napake. Obrnite se na skrbnika sistema." + }, + { + "id": "ent.saml.attribute.app_error", + "translation": "Prijava SAML ni uspela, ker je eden od atributov napačen. Obrnite se na skrbnika sistema." + }, + { + "id": "ent.migration.migratetosaml.username_already_used_by_other_user", + "translation": "Uporabniško ime že uporablja drug Mattermost uporabnik." + }, + { + "id": "ent.migration.migratetosaml.user_not_found_in_users_mapping_file", + "translation": "Uporabnika ni mogoče najti v datoteki uporabnikov." + }, + { + "id": "ent.migration.migratetosaml.email_already_used_by_other_user", + "translation": "E-pošto že uporablja drug uporabnik SAML." + }, + { + "id": "ent.migration.migratetoldap.user_not_found", + "translation": "Uporabnika na strežniku AD / LDAP ni mogoče najti: " + }, + { + "id": "ent.migration.migratetoldap.duplicate_field", + "translation": "Uporabnikov AD / LDAP z navedenim poljem ni mogoče preseliti. Zaznan je bil podvojen vnos. Odstranite vsa dvojna besedila in poskusite znova." + }, + { + "id": "ent.message_export.global_relay_export.deliver.unable_to_open_zip_file_data.app_error", + "translation": "Ni mogoče odpreti začasne datoteke za izvoz" + }, + { + "id": "ent.message_export.global_relay_export.deliver.unable_to_open_email_file.app_error", + "translation": "E-poštnega sporočila iz začasne datoteke ni mogoče dobiti" + }, + { + "id": "ent.message_export.global_relay_export.deliver.unable_to_get_file_info.app_error", + "translation": "Podatkov o izvozni začasni datoteki ni mogoče dobiti" + }, + { + "id": "ent.message_export.global_relay_export.deliver.to_address.app_error", + "translation": "Ni mogoče nastaviti e-poštni Za naslov." + }, + { + "id": "ent.message_export.global_relay_export.deliver.parse_mail.app_error", + "translation": "Ni možno prebrati informacij o e-pošti." + }, + { + "id": "ent.message_export.global_relay_export.deliver.msg_data.app_error", + "translation": "E-poštnega sporočila ni mogoče napisati." + }, + { + "id": "ent.message_export.global_relay_export.deliver.msg.app_error", + "translation": "E-poštnega sporočila ni mogoče nastaviti." + }, + { + "id": "ent.message_export.global_relay_export.deliver.from_address.app_error", + "translation": "Ni mogoče nastaviti e-poštni Od naslov." + }, + { + "id": "ent.message_export.global_relay.generate_email.app_error", + "translation": "Podatkov o datoteki eml ni mogoče ustvariti." + }, + { + "id": "ent.message_export.global_relay.create_file_in_zip.app_error", + "translation": "Datoteke eml ni mogoče ustvariti." + }, + { + "id": "ent.message_export.global_relay.close_zip_file.app_error", + "translation": "Datoteke zip ni mogoče zapreti." + }, + { + "id": "ent.message_export.global_relay.attach_file.app_error", + "translation": "Priloge ni mogoče dodati izvozu CSV." + }, + { + "id": "ent.ldap_groups.reachable_groups_error", + "translation": "napaka pri pridobivanju skupin za uporabnika" + }, + { + "id": "ent.ldap_groups.no_rows", + "translation": "z ujemajočim uid-om ni bilo najdenih skupin" + }, + { + "id": "ent.ldap_groups.members_of_group_error", + "translation": "napaka pri pridobivanju članov skupine" + }, + { + "id": "ent.ldap_groups.groups_search_error", + "translation": "napaka pri pridobivanju ldap skupin" + }, + { + "id": "ent.ldap_groups.group_search_error", + "translation": "napaka pri pridobivanju skupine ldap" + }, + { + "id": "ent.ldap.validate_guest_filter.app_error", + "translation": "Neveljaven filter AD / LDAP" + }, + { + "id": "ent.ldap.validate_filter.app_error", + "translation": "Neveljaven filter AD / LDAP" + }, + { + "id": "ent.ldap.validate_admin_filter.app_error", + "translation": "Neveljaven filter AD / LDAP" + }, + { + "id": "ent.ldap.syncronize.search_failure.app_error", + "translation": "Iskanje uporabnikov v AD / LDAP ni uspelo. Preverite, ali se strežnik Mattermost lahko poveže z vašim strežnikom AD / LDAP in poskusite znova." + }, + { + "id": "ent.ldap.syncronize.get_all_groups.app_error", + "translation": "napaka pri pridobivanju skupin" + }, + { + "id": "ent.ldap.syncronize.get_all.app_error", + "translation": "Ni mogoče dobiti vseh uporabnikov, ki uporabljajo AD / LDAP" + }, + { + "id": "ent.ldap.do_login.user_not_registered.app_error", + "translation": "Uporabnik ni registriran na strežniku AD / LDAP" + }, + { + "id": "ent.ldap.do_login.user_filtered.app_error", + "translation": "Vaš račun AD/LDAP nima dovoljenja za uporabo tega strežnika Mattermost. Prosite skrbnika sistema, da preveri filter uporabnikov AD/LDAP." + }, + { + "id": "ent.ldap.do_login.unable_to_connect.app_error", + "translation": "Ni mogoče vzpostaviti povezave s strežnikom AD/LDAP" + }, + { + "id": "ent.ldap.do_login.search_ldap_server.app_error", + "translation": "Iskanje strežnika AD/LDAP ni uspelo" + }, + { + "id": "ent.ldap.do_login.matched_to_many_users.app_error", + "translation": "Podano uporabniško ime se ujema z več uporabniki" + }, + { + "id": "ent.ldap.do_login.licence_disable.app_error", + "translation": "Funkcija AD/LDAP je onemogočena s trenutno licenco. O nadgradnji licence za podjetje se obrnite na skrbnika sistema." + }, + { + "id": "ent.ldap.do_login.invalid_password.app_error", + "translation": "Neveljavno geslo" + }, + { + "id": "ent.ldap.do_login.bind_admin_user.app_error", + "translation": "Povezava s strežnikom AD/LDAP ni možna. Preverite BindUsername in BindPassword." + }, + { + "id": "ent.ldap.disabled.app_error", + "translation": "AD/LDAP je onemogočen ali licenca ne podpira AD/LDAP." + }, + { + "id": "ent.ldap.create_fail", + "translation": "Uporabnika LDAP ni mogoče ustvariti." + }, + { + "id": "ent.elasticsearch.test_config.reenter_password", + "translation": "URL Elasticsearch strežnika ali uporabniško ime je spremenjeno. Če želite preskusiti povezavo, ponovno vnesite geslo Elasticsearch." + }, + { + "id": "ent.elasticsearch.test_config.license.error", + "translation": "Vaša licenca ne podpira Elasticsearch." + }, + { + "id": "ent.elasticsearch.test_config.indexing_disabled.error", + "translation": "Elasticsearch je onemogočen." + }, + { + "id": "ent.elasticsearch.stop.already_stopped.app_error", + "translation": "Elasticsearch je že ustavljen" + }, + { + "id": "ent.elasticsearch.start.already_started.app_error", + "translation": "Elasticsearch je že zagnan" + }, + { + "id": "ent.elasticsearch.search_users.unmarshall_user_failed", + "translation": "Napaka pri dekodiranju rezultatov iskanja" + }, + { + "id": "ent.elasticsearch.search_users.search_failed", + "translation": "Iskanje ni bilo mogoče dokončati" + }, + { + "id": "ent.elasticsearch.search_posts.unmarshall_post_failed", + "translation": "Napaka pri dekodiranju rezultatov iskanja" + }, + { + "id": "ent.elasticsearch.search_posts.search_failed", + "translation": "Iskanje ni bilo mogoče dokončati" + }, + { + "id": "ent.elasticsearch.search_posts.parse_matches_failed", + "translation": "Podatkov o rezultatih iskanja ni bilo mogoče razčleniti" + }, + { + "id": "ent.elasticsearch.search_posts.disabled", + "translation": "Elasticsearch iskanje je onemogočeno na tem strežniku" + }, + { + "id": "ent.elasticsearch.search_channels.unmarshall_channel_failed", + "translation": "Napaka pri dekodiranju rezultatov iskanja" + }, + { + "id": "ent.elasticsearch.search_channels.search_failed", + "translation": "Iskanje ni bilo mogoče dokončati" + }, + { + "id": "ent.elasticsearch.search_channels.disabled", + "translation": "Elasticsearch iskanje je onemogočeno na tem strežniku" + }, + { + "id": "ent.elasticsearch.refresh_indexes.refresh_failed", + "translation": "Napaka pri pridobivanju Elasticsearch indeksov" + }, + { + "id": "ent.elasticsearch.purge_indexes.delete_failed", + "translation": "Indeks Elasticsearch ni bil izbrisan" + }, + { + "id": "ent.elasticsearch.not_started.error", + "translation": "Elasticsearch ni zagnan" + }, + { + "id": "ent.elasticsearch.index_user.error", + "translation": "Objave ni bilo mogoče indeksirati" + }, + { + "id": "ent.elasticsearch.index_post.error", + "translation": "Objave ni bilo mogoče indeksirati" + }, + { + "id": "ent.elasticsearch.index_channel.error", + "translation": "Posodobitev kanala ni uspela" + }, + { + "id": "ent.elasticsearch.generic.disabled", + "translation": "Elasticsearch iskanje ni omogočeno na tem strežniku" + }, + { + "id": "ent.elasticsearch.delete_user.error", + "translation": "Objave ni bilo mogoče izbrisati" + }, + { + "id": "ent.elasticsearch.delete_post.error", + "translation": "Objave ni bilo mogoče izbrisati" + }, + { + "id": "ent.elasticsearch.delete_channel.error", + "translation": "Posodobitev kanala ni uspela" + }, + { + "id": "ent.elasticsearch.data_retention_delete_indexes.get_indexes.error", + "translation": "Napaka pri pridobivanju Elasticsearch indeksov" + }, + { + "id": "ent.elasticsearch.data_retention_delete_indexes.delete_index.error", + "translation": "Napaka pri brisanju Elasticsearch indeksa" + }, + { + "id": "ent.elasticsearch.create_client.connect_failed", + "translation": "Nastavitev odjemalca Elasticsearch ni uspela" + }, + { + "id": "ent.data_retention.generic.license.error", + "translation": "Vaša licenca ne podpira hrambe podatkov." + }, + { + "id": "ent.compliance.run_failed.error", + "translation": "Izvoz skladnosti ni uspel za opravilo '{{.JobName}}' na '{{.FilePath}}'" + }, + { + "id": "ent.compliance.run_export.template_watcher.appError", + "translation": "Ni možno naložiti predlog za izvoz. Prosim poizkusite ponovno." + }, + { + "id": "ent.compliance.licence_disable.app_error", + "translation": "Funkcija skladnosti je onemogočena s trenutno licenco. O nadgradnji licence za podjetje se obrnite na skrbnika sistema." + }, + { + "id": "ent.compliance.global_relay.open_temporary_file.appError", + "translation": "Začasne izvozne datoteke ni mogoče odpreti." + }, + { + "id": "ent.compliance.global_relay.attachments_removed.appError", + "translation": "Naložena datoteka je bila odstranjena iz izvoza Global Relay, ker je bila prevelika za pošiljanje." + }, + { + "id": "ent.compliance.csv.zip.creation.appError", + "translation": "Izvozne datoteke zip ni mogoče ustvariti." + }, + { + "id": "ent.compliance.csv.warning.appError", + "translation": "Datoteke eml ni mogoče ustvariti." + }, + { + "id": "ent.compliance.csv.post.export.appError", + "translation": "Objave ni mogoče izvoziti." + }, + { + "id": "app.scheme.save.invalid_scheme.app_error", + "translation": "Navedena shema ni veljavna" + }, + { + "id": "app.scheme.save.app_error", + "translation": "Sheme ni mogoče ustvariti" + }, + { + "id": "app.scheme.permanent_delete_all.app_error", + "translation": "Shem ni bilo mogoče trajno izbrisati" + }, + { + "id": "app.scheme.get.app_error", + "translation": "Ni mogoče dobiti sheme" + }, + { + "id": "app.reaction.save.save.app_error", + "translation": "Reakcije ni mogoče shraniti" + }, + { + "id": "app.reaction.delete_all_with_emoji_name.get_reactions.app_error", + "translation": "Ni mogoče dobiti vseh reakcij s tem imenom čustvenega simbola" + }, + { + "id": "store.sql_post.search.disabled", + "translation": "Iskanje je bilo na tem strežniku onemogočeno. Obrnite se na skrbnika sistema." + }, + { + "id": "ent.compliance.csv.metadata.json.zipfile.appError", + "translation": "Datoteke eml ni mogoče ustvariti." + }, + { + "id": "ent.compliance.csv.metadata.json.marshalling.appError", + "translation": "Metapodatkov ni mogoče pretvoriti v json." + }, + { + "id": "ent.compliance.csv.metadata.export.appError", + "translation": "Datoteke z metapodatki ni mogoče dodati v datoteko zip." + }, + { + "id": "ent.compliance.csv.header.export.appError", + "translation": "Glave ni mogoče dodati izvozu CSV." + }, + { + "id": "ent.compliance.csv.file.creation.appError", + "translation": "Ni mogoče ustvariti začasne izvozne datoteke CSV." + }, + { + "id": "ent.compliance.csv.attachment.export.appError", + "translation": "Priloge ni mogoče dodati izvozu CSV." + }, + { + "id": "ent.compliance.csv.attachment.copy.appError", + "translation": "Priloge ni mogoče kopirati v datoteko zip." + }, + { + "id": "ent.compliance.bad_export_type.appError", + "translation": "Neznana izhodna oblika {{.ExportType}}" + }, + { + "id": "ent.cluster.save_config.error", + "translation": "Sistemska konzola je nastavljena samo za branje, če je omogočena visoka razpoložljivost, razen če je v konfiguracijski datoteki onemogočeno ReadOnlyConfig." + }, + { + "id": "ent.cluster.config_changed.info", + "translation": "Konfiguracija gruče se je spremenila za id = {{.id}}. Gruča lahko postane nestabilna in potreben je ponovni zagon. Če želite zagotoviti, da je gruča pravilno konfigurirana, morate nemudoma znova izvesti ponovni zagon." + }, + { + "id": "ent.account_migration.get_saml_users_failed", + "translation": "Uporabnikov SAML ni mogoče dobiti." + }, + { + "id": "ent.account_migration.get_all_failed", + "translation": "Uporabnikov ni mogoče dobiti." + }, + { + "id": "brand.save_brand_image.save_image.app_error", + "translation": "Slikovne datoteke ni mogoče zapisati v shrambo datotek. Preverite povezavo in poskusite znova." + }, + { + "id": "brand.save_brand_image.open.app_error", + "translation": "Ne morem prenesti slike blagovne znamke po meri. Prepričajte se, da je velikost slike manjša od 2 MB in poskusite znova." + }, + { + "id": "brand.save_brand_image.encode.app_error", + "translation": "Slikovnih podatkov ni mogoče pretvoriti v format PNG. Prosim poskusite ponovno." + }, + { + "id": "brand.save_brand_image.decode.app_error", + "translation": "Podatkov o sliki ni mogoče dekodirati." + }, + { + "id": "bleveengine.search_users_in_team.error", + "translation": "Iskanje ni bilo mogoče dokončati" + }, + { + "id": "bleveengine.search_users_in_channel.uchan.error", + "translation": "Iskanje ni bilo mogoče dokončati" + }, + { + "id": "bleveengine.search_users_in_channel.nuchan.error", + "translation": "Iskanje ni bilo mogoče dokončati" + }, + { + "id": "bleveengine.search_posts.error", + "translation": "Iskanje ni bilo mogoče dokončati" + }, + { + "id": "bleveengine.search_channels.error", + "translation": "Iskanje ni bilo mogoče dokončati" + }, + { + "id": "bleveengine.index_user.error", + "translation": "Objave ni bilo mogoče indeksirati" + }, + { + "id": "bleveengine.index_post.error", + "translation": "Objave ni bilo mogoče indeksirati" + }, + { + "id": "bleveengine.index_channel.error", + "translation": "Posodobitev kanala ni uspela" + }, + { + "id": "bleveengine.delete_user.error", + "translation": "Objave ni bilo mogoče izbrisati" + }, + { + "id": "bleveengine.delete_post.error", + "translation": "Objave ni bilo mogoče izbrisati" + }, + { + "id": "bleveengine.delete_channel.error", + "translation": "Posodobitev kanala ni uspela" + }, + { + "id": "app.user_access_token.invalid_or_missing", + "translation": "Neveljaven ali manjkajoč žeton" + }, + { + "id": "app.user_access_token.disabled", + "translation": "Osebni žetoni za dostop so onemogočeni na tem strežniku. Za podrobnosti se obrnite na skrbnika sistema." + }, + { + "id": "app.team.join_user_to_team.max_accounts.app_error", + "translation": "Ta ekipa je dosegla največje dovoljeno število računov. Obrnite se na skrbnika sistema, da nastavite višjo omejitev." + }, + { + "id": "app.system_install_date.parse_int.app_error", + "translation": "Datum namestitve ni bil razčlenjen" + }, + { + "id": "app.submit_interactive_dialog.json_error", + "translation": "Prišlo je do napake pri kodiranju JSON za interaktivni dialog." + }, + { + "id": "app.schemes.is_phase_2_migration_completed.not_completed.app_error", + "translation": "Ta končna točka API ni dostopna, ker zahtevane migracije še niso dokončane." + }, + { + "id": "app.role.check_roles_exist.role_not_found", + "translation": "Zagotovljena vloga ne obstaja" + }, + { + "id": "app.plugin.write_file.saving.app_error", + "translation": "Napaka med zapuščanjem kanala." + }, + { + "id": "app.plugin.write_file.read.app_error", + "translation": "Napaka med zapuščanjem kanala." + }, + { + "id": "app.plugin.upload_disabled.app_error", + "translation": "Vstavki in / ali prenosi vtičnikov so onemogočeni." + }, + { + "id": "app.plugin.remove.app_error", + "translation": "Vtičnika ni mogoče izbrisati" + }, + { + "id": "app.plugin.not_installed.app_error", + "translation": "Vtičnik ni nameščen" + }, + { + "id": "app.plugin.mvdir.app_error", + "translation": "Vtičnika iz začasne mape ni mogoče premakniti v končno mapo. Drugi vtičnik morda uporablja isto ime mape." + }, + { + "id": "app.plugin.marketplace_disabled.app_error", + "translation": "Vtičniki so onemogočeni. Za podrobnosti preverite svoje dnevnike." + }, + { + "id": "app.plugin.manifest.app_error", + "translation": "Manifesta za izvoženi vtičnik ni mogoče najti" + }, + { + "id": "app.plugin.invalid_id.app_error", + "translation": "ID vtičnika mora biti vsaj {{.Min}} znakov, največ {{.Max}} znakov in se ujema z {{.Regex}}." + }, + { + "id": "app.plugin.install_id_failed_remove.app_error", + "translation": "Vtičnika ni mogoče namestiti. Vtičnik z istim ID-jem je že nameščen in ga ni mogoče odstraniti." + }, + { + "id": "app.plugin.install_id.app_error", + "translation": "Vtičnika ni mogoče namestiti. Vtičnik z istim ID-jem je že nameščen." + }, + { + "id": "app.plugin.install.app_error", + "translation": "Vtičnika ni mogoče namestiti." + }, + { + "id": "app.plugin.get_statuses.app_error", + "translation": "Stanj vtičnika ni mogoče dobiti" + }, + { + "id": "app.plugin.get_plugins.app_error", + "translation": "Aktivnih vtičnikov ni mogoče dobiti" + }, + { + "id": "app.plugin.get_cluster_plugin_statuses.app_error", + "translation": "Stanj vtičnikov iz gruče ni mogoče dobiti." + }, + { + "id": "app.plugin.filesystem.app_error", + "translation": "Prišlo je do napake datotečnega sistema" + }, + { + "id": "app.plugin.disabled.app_error", + "translation": "Vtičniki so onemogočeni. Za podrobnosti preverite svoje dnevnike." + }, + { + "id": "app.plugin.delete_public_key.delete.app_error", + "translation": "Pri brisanju certifikata je prišlo do napake." + }, + { + "id": "app.plugin.deactivate.app_error", + "translation": "Vtičnika ni mogoče deaktivirati" + }, + { + "id": "app.plugin.config.app_error", + "translation": "Napaka pri shranjevanju stanja vtičnika v config" + }, + { + "id": "app.plugin.cluster.save_config.app_error", + "translation": "Konfiguracijo vtičnika v datoteki config.json morate ročno posodobiti, ko uporabljate program ReadOnlyConfig z omogočenim združevanjem v gruče." + }, + { + "id": "app.notification.subject.notification.full", + "translation": "[{{ .SiteName }}] Obvestilo v {{ .TeamName}} dne {{.Day}} {{.Month}}, {{.Year}}" + }, + { + "id": "app.notification.subject.group_message.generic", + "translation": "[{{ .SiteName }}] Novo sporočilo skupine dne {{.Day}} {{.Month}}, {{.Year}}" + }, + { + "id": "app.notification.subject.group_message.full", + "translation": "[{{ .SiteName }}] Novo sporočilo skupine v {{ .ChannelName}} dne {{.Day}} {{.Month}}, {{.Year}}" + }, + { + "id": "app.notification.subject.direct.full", + "translation": "[{{.SiteName}}] Novo obvestilo od {{.SenderDisplayName}} dne {{.Day}} {{.Month}}, {{.Year}}" + }, + { + "id": "app.import.validate_user_teams_import_data.team_name_missing.error", + "translation": "Ime ekipe manjka v uporabnikovem članstvu v ekipi." + }, + { + "id": "app.import.validate_user_teams_import_data.invalid_roles.error", + "translation": "Neveljavne vloge za uporabnikovo članstvo v ekipi." + }, + { + "id": "app.import.validate_user_import_data.username_missing.error", + "translation": "Manjkajo zahteve za lastnost uporabnika: uporabniško ime." + }, + { + "id": "app.import.validate_user_import_data.username_invalid.error", + "translation": "Uporabniško ime ni veljavno." + }, + { + "id": "app.import.validate_user_import_data.roles_invalid.error", + "translation": "Vloge uporabnika niso veljavne." + }, + { + "id": "app.import.validate_user_import_data.profile_image.error", + "translation": "Neveljavna slika profila." + }, + { + "id": "app.import.validate_user_import_data.position_length.error", + "translation": "Položaj uporabnika je predolg." + }, + { + "id": "app.import.validate_user_import_data.password_length.error", + "translation": "Uporabniško geslo ima neveljavno dolžino." + }, + { + "id": "app.import.validate_user_import_data.nickname_length.error", + "translation": "Vzdevek uporabnika je predolg." + }, + { + "id": "app.import.validate_user_import_data.last_name_length.error", + "translation": "Priimek za uporabnika je predolg." + }, + { + "id": "app.import.validate_user_import_data.first_name_length.error", + "translation": "Ime uporabnika je predolgo." + }, + { + "id": "app.import.validate_user_import_data.email_missing.error", + "translation": "Manjka zahtevana lastnost uporabnika: e-pošta." + }, + { + "id": "app.import.validate_user_import_data.email_length.error", + "translation": "Uporabnikova e-pošta ima neveljavno dolžino." + }, + { + "id": "app.import.validate_user_import_data.auth_data_length.error", + "translation": "Uporabnikova AuthData je predolga." + }, + { + "id": "app.import.validate_user_import_data.auth_data_and_password.error", + "translation": "Uporabnikova AuthData in geslo se med seboj izključujeta." + }, + { + "id": "app.import.validate_user_channels_import_data.invalid_roles.error", + "translation": "Neveljavne vloge za članstvo uporabnika v kanalu." + }, + { + "id": "app.import.validate_user_channels_import_data.invalid_notify_props_mobile.error", + "translation": "Neveljaven mobilni NotifyProps za članstvo uporabnikovega kanala." + }, + { + "id": "app.import.validate_user_channels_import_data.invalid_notify_props_mark_unread.error", + "translation": "Neveljaven MarkUnread NotifyProps za članstvo uporabnika kanala." + }, + { + "id": "app.import.validate_user_channels_import_data.invalid_notify_props_desktop.error", + "translation": "Neveljavne vloge za članstvo uporabnika v kanalu." + }, + { + "id": "app.import.validate_user_channels_import_data.channel_name_missing.error", + "translation": "V uporabniškem članskem kanalu manjka ime kanala." + }, + { + "id": "app.import.validate_team_import_data.type_missing.error", + "translation": "Manjka zahtevana lastnost ekipe: type." + }, + { + "id": "app.import.validate_team_import_data.type_invalid.error", + "translation": "Vrsta ekipe ni veljavna." + }, + { + "id": "app.import.validate_team_import_data.scheme_invalid.error", + "translation": "Neveljavno ime sheme za ekipo." + }, + { + "id": "app.import.validate_team_import_data.name_reserved.error", + "translation": "Ime ekipevsebuje rezervirane besede." + }, + { + "id": "app.import.validate_team_import_data.name_missing.error", + "translation": "Manjka zahtevana lastnost ekipe: name." + }, + { + "id": "app.import.validate_team_import_data.name_length.error", + "translation": "Ime ekipe je predolgo." + }, + { + "id": "app.import.import_line.null_post.error", + "translation": "Vrstica za uvoz podatkov ima vrsto \"objava\", vendar ima predmet objave vrednost null." + }, + { + "id": "app.import.import_line.null_emoji.error", + "translation": "Vrstica za uvoz podatkov ima tip \"čustvenega simbola\", vrednost objekta emoji pa je null." + }, + { + "id": "app.import.import_line.null_direct_post.error", + "translation": "Vrstica za uvoz podatkov ima tip \"direct_post\", vendar ima objekt direct_post vrednost null." + }, + { + "id": "app.import.import_line.null_direct_channel.error", + "translation": "Vrstica za uvoz podatkov ima tip \"direct_channel\", vendar ima objekt direct_channel vrednost null." + }, + { + "id": "app.import.import_line.null_channel.error", + "translation": "Vrstica za uvoz podatkov ima tip \"channel\", vendar ima objekt kanala vrednost null." + }, + { + "id": "app.import.import_direct_post.create_group_channel.error", + "translation": "Napaka pri pridobitvi kanala skupine" + }, + { + "id": "app.import.import_direct_post.create_direct_channel.error", + "translation": "Ni bilo mogoče dobiti neposrednega kanala" + }, + { + "id": "app.import.import_direct_channel.update_header_failed.error", + "translation": "Posodobitev neposredne glave kanala ni uspela" + }, + { + "id": "app.import.import_direct_channel.create_group_channel.error", + "translation": "Napaka pri kreiranju skupinskega kanala" + }, + { + "id": "app.import.import_direct_channel.create_direct_channel.error", + "translation": "Napaka pri kreiranju direktnega kanala" + }, + { + "id": "app.import.import_channel.team_not_found.error", + "translation": "Napaka pri uvažanju kanala. Skupine z imenom \"{{.TeamName}}\" ni bilo mogoče najti." + }, + { + "id": "app.import.import_channel.scheme_wrong_scope.error", + "translation": "Kanal mora biti dodeljen shemi, ki je določena v kanalu." + }, + { + "id": "app.import.import_channel.scheme_deleted.error", + "translation": "Ni možno uporabiti izbrisano shemo za nastavitev kanala." + }, + { + "id": "app.import.emoji.bad_file.error", + "translation": "Napaka pri branju uvozne slikovne datoteke emoji. Emoji z imenom: \"{{.EmojiName}}\"" + }, + { + "id": "app.import.bulk_import.unsupported_version.error", + "translation": "Nepravilna ali manjkajoča različica v datoteki za uvoz podatkov. Poskrbite, da je različica prvi objekt v uvozni datoteki in poskusite znova." + }, + { + "id": "app.import.bulk_import.json_decode.error", + "translation": "Napaka pri dekodiranju JSON vrstice." + }, + { + "id": "app.import.bulk_import.file_scan.error", + "translation": "Napaka pri branju datoteke za uvoz." + }, + { + "id": "app.import.attachment.file_upload.error", + "translation": "Napaka pri nalaganju datoteke: \"{{.FilePath}}\"" + }, + { + "id": "app.import.attachment.bad_file.error", + "translation": "Napaka pri branju datoteke: \"{{.FilePath}}\"" + }, + { + "id": "app.export.export_write_line.io_writer.error", + "translation": "Pri pisanju izvoznih podatkov je prišlo do napake." + }, + { + "id": "app.channel.post_update_channel_purpose_message.updated_to", + "translation": "%s je posodobil sporočilo kanala v: %s" + }, + { + "id": "app.channel.post_update_channel_purpose_message.updated_from", + "translation": "%s je posodobil sporočilo kanala iz: %s v: %s" + }, + { + "id": "app.channel.post_update_channel_purpose_message.retrieve_user.error", + "translation": "Napaka pridobivanja uporabnika med posodabljanjem namena kanala %v" + }, + { + "id": "app.channel.post_update_channel_purpose_message.removed", + "translation": "%s je odstranil sporočila kanala (prej: %s)" + }, + { + "id": "app.channel.post_update_channel_purpose_message.post.error", + "translation": "Sporočila o namenu kanala ni bilo mogoče objaviti" + }, + { + "id": "app.channel.move_channel.members_do_not_match.error", + "translation": "Kanala ni mogoče premakniti, razen če so vsi člani že člani ciljne ekipe." + }, + { + "id": "app.channel.create_direct_channel.internal_error", + "translation": "Kanala ni mogoče shraniti" + }, + { + "id": "app.channel.create_channel.no_team_id.app_error", + "translation": "Potrebno je specificirati ID ekipe za kreiranje kanala" + }, + { + "id": "app.channel.create_channel.internal_error", + "translation": "Kanala ni mogoče shraniti" + }, + { + "id": "app.bot.permenent_delete.bad_id", + "translation": "Objave ni mogoče izbrisati" + }, + { + "id": "app.bot.patchbot.internal_error", + "translation": "Objave ni mogoče posodobiti" + }, + { + "id": "app.bot.getbots.internal_error", + "translation": "Objav ni mogoče dobiti" + }, + { + "id": "app.bot.getbot.internal_error", + "translation": "Objave ni mogoče dobiti" + }, + { + "id": "app.bot.createbot.internal_error", + "translation": "Objave ni mogoče shraniti" + }, + { + "id": "app.admin.test_email.failure", + "translation": "Neuspešna povezava: {{.Error}}" + }, + { + "id": "api.websocket_handler.invalid_param.app_error", + "translation": "Napačen parameter {{.Name}}" + }, + { + "id": "api.webhook.team_mismatch.app_error", + "translation": "Ni mogoče posodobiti spletnih zank čez ekipe" + }, + { + "id": "api.webhook.create_outgoing.triggers.app_error", + "translation": "Potrebno je nastaviti trigger_words ali channel_id" + }, + { + "id": "api.user.verify_email.broken_token.app_error", + "translation": "Neveljavno preverjanje vrste e-poštnega žetona." + }, + { + "id": "api.user.verify_email.bad_link.app_error", + "translation": "Neveljavna povezava za potrditev e-pošte." + }, + { + "id": "api.user.upload_profile_user.upload_profile.app_error", + "translation": "Ni možno naložiti profilne slike" + }, + { + "id": "api.user.upload_profile_user.too_large.app_error", + "translation": "Napaka pri nalaganju slike. Datoteka je prevelika." + }, + { + "id": "api.user.upload_profile_user.storage.app_error", + "translation": "Datoteke ni mogoče naložiti. Shramba ni nastavljena." + }, + { + "id": "api.user.upload_profile_user.parse.app_error", + "translation": "Ni mogoče prebrati multipart obrazca" + }, + { + "id": "api.user.upload_profile_user.open.app_error", + "translation": "Slikovne datoteke ni bilo mogoče odpreti" + }, + { + "id": "api.user.upload_profile_user.no_file.app_error", + "translation": "Ni datoteke v polju 'slika' v zahtevi" + }, + { + "id": "api.user.upload_profile_user.encode.app_error", + "translation": "Profilne slike ni možno kodirati" + }, + { + "id": "api.user.upload_profile_user.decode.app_error", + "translation": "Ni mogoče dekodirati profilne slike" + }, + { + "id": "api.user.upload_profile_user.array.app_error", + "translation": "Prazen seznam v polju 'image' v zahtevi" + }, + { + "id": "api.user.update_user.accepted_guest_domain.app_error", + "translation": "Vneseno e-poštno sporočilo ne pripada sprejeti domeni. Obrnite se na skrbnika ali se prijavite z drugim naslovom e-pošte." + }, + { + "id": "api.user.update_user.accepted_domain.app_error", + "translation": "Vneseno e-poštno sporočilo ne pripada sprejeti domeni. Obrnite se na skrbnika ali se prijavite z drugim naslovom e-pošte." + }, + { + "id": "api.user.update_password.valid_account.app_error", + "translation": "Sprememba gesla ni možna, ker ni mogoče najti velljavnega uporabniškega računa" + }, + { + "id": "api.user.update_password.oauth.app_error", + "translation": "Geslo za posodobitev ni uspelo, ker je uporabnik prijavljen preko storitve OAuth" + }, + { + "id": "api.user.update_password.menu", + "translation": "uporablja meni za nastavitve" + }, + { + "id": "api.user.update_password.incorrect.app_error", + "translation": "Vnešeno \"staro geslo\" ni pravilno. Prosim preverite, da je Caps Lock izklopljen in poizkusite znova." + }, + { + "id": "api.user.update_password.failed.app_error", + "translation": "Posodobitev gesla ni uspela" + }, + { + "id": "api.user.update_password.context.app_error", + "translation": "Geslo za posodobitev ni uspelo, ker se kontekst user_id ni ujemal z rekviziti user_id" + }, + { + "id": "api.user.update_oauth_user_attrs.get_user.app_error", + "translation": "Uporabnika iz uporabniškega objekta {{.Service}} ni bilo mogoče dobiti" + }, + { + "id": "api.user.update_active.permissions.app_error", + "translation": "Nimate ustreznih dovoljenj" + }, + { + "id": "api.user.update_active.not_enable.app_error", + "translation": "Ni možno onemogočiti lastnega uporabniškega računa, ker ta možnost ni omogočena. Prosim kontaktirajte svoje skrbnika sistema." + }, + { + "id": "api.user.send_verify_email_and_forget.failed.error", + "translation": "Neuspešno pošiljanje e-pošte za preverjanje" + }, + { + "id": "api.user.send_sign_in_change_email_and_forget.error", + "translation": "Neuspešno pošiljanje e-pošte za posodobitev e-pošte" + }, + { + "id": "api.user.send_password_reset.sso.app_error", + "translation": "Gesla za račune SSO ni mogoče ponastaviti" + }, + { + "id": "api.user.send_password_reset.send.app_error", + "translation": "Neuspešno pošiljanje e-pošte za ponastavitev gesla" + }, + { + "id": "api.user.send_email_change_verify_email_and_forget.error", + "translation": "Neuspešno pošiljanje e-pošte za spremenitev e-pošte za preverjanje" + }, + { + "id": "api.user.send_deactivate_email_and_forget.failed.error", + "translation": "Neuspešno pošiljanje e-pošte za deaktiviranje računa" + }, + { + "id": "api.user.saml.not_available.app_error", + "translation": "SAML 2.0 ni podprt ali nastavljen na strežniku." + }, + { + "id": "api.user.reset_password.sso.app_error", + "translation": "Gesla za račune SSO ni mogoče ponastaviti" + }, + { + "id": "api.user.reset_password.method", + "translation": "uporablja povezavo za ponastavitev gesla" + }, + { + "id": "api.user.reset_password.link_expired.app_error", + "translation": "Povezava za ponastavitev gesla je potekla." + }, + { + "id": "api.user.reset_password.invalid_link.app_error", + "translation": "Povezava za ponastavitev gesla ni veljavna." + }, + { + "id": "api.user.reset_password.broken_token.app_error", + "translation": "Žeton za ponastavitev gesla ni veljaven." + }, + { + "id": "api.user.oauth_to_email.not_available.app_error", + "translation": "Prenos za preverjanje pristnosti ni konfiguriran ali na voljo v tem strežniku." + }, + { + "id": "api.user.oauth_to_email.context.app_error", + "translation": "Geslo za posodobitev ni uspelo, ker se kontekst user_id ni ujemal s podanim ID-jem uporabnika" + }, + { + "id": "api.user.login_ldap.not_available.app_error", + "translation": "AD / LDAP ni na voljo na tem strežniku" + }, + { + "id": "api.user.login_by_oauth.parse.app_error", + "translation": "Podatkov za avtorizacijo ni bilo mogoče razčleniti iz uporabniškega objekta {{.Service}}" + }, + { + "id": "api.user.login_by_oauth.not_available.app_error", + "translation": "{{.Service}} SSO prek OAuth 2.0 ni na voljo na tem strežniku" + }, + { + "id": "api.user.login.use_auth_service.app_error", + "translation": "Prosim prijavite se z {{.AuthService}}" + }, + { + "id": "api.user.login.not_verified.app_error", + "translation": "Prijava ni uspela, ker e-poštni naslov ni bil preverjen" + }, + { + "id": "api.user.login.inactive.app_error", + "translation": "Prijava ni uspela, ker je bil vaš račun deaktiviran. Obrnite se na skrbnika." + }, + { + "id": "api.user.login.guest_accounts.license.error", + "translation": "vaša licenca ne podpira skupin ldap" + }, + { + "id": "api.user.login.client_side_cert.license.app_error", + "translation": "Poskušali ste uporabiti poskusno funkcijo ClientSideCertEnable brez veljavne licence podjetja" + }, + { + "id": "api.user.login.client_side_cert.certificate.app_error", + "translation": "Poskušali ste se prijaviti z eksperimentalno funkcijo ClientSideCert, ne da bi zagotovili veljaven certifikat" + }, + { + "id": "api.user.login.blank_pwd.app_error", + "translation": "Polje za vnos gesla ne sme biti prazno" + }, + { + "id": "api.user.ldap_to_email.not_ldap_account.app_error", + "translation": "Ta uporabniški račun ne uporablja AD / LDAP" + }, + { + "id": "api.user.ldap_to_email.not_available.app_error", + "translation": "AD / LDAP ni na voljo na tem strežniku" + }, + { + "id": "api.user.get_user_by_email.permissions.app_error", + "translation": "Uporabnika ni mogoče dobiti po e-pošti." + }, + { + "id": "api.user.email_to_oauth.not_available.app_error", + "translation": "Prenos za preverjanje pristnosti ni konfiguriran ali na voljo v tem strežniku." + }, + { + "id": "api.user.email_to_ldap.not_available.app_error", + "translation": "AD / LDAP ni na voljo na tem strežniku" + }, + { + "id": "api.user.create_user.signup_link_invalid.app_error", + "translation": "Prijavna povezava ni veljavna" + }, + { + "id": "api.user.create_user.signup_link_expired.app_error", + "translation": "Povezava za prijavo je potekla" + }, + { + "id": "api.user.create_user.signup_email_disabled.app_error", + "translation": "Prijava uporabnikov z e-pošto je onemogočena." + }, + { + "id": "api.user.create_user.no_open_server", + "translation": "Ta strežnik ne dovoljuje odprtih prijav. Če želite prejeti vabilo, se pogovorite z vašim skrbnikom." + }, + { + "id": "api.user.create_user.guest_accounts.license.app_error", + "translation": "vaša licenca ne podpira skupin ldap" + }, + { + "id": "api.user.create_user.disabled.app_error", + "translation": "Ustvarjanje uporabnikov je onemogočeno." + }, + { + "id": "api.user.create_user.accepted_domain.app_error", + "translation": "Vneseno e-poštno sporočilo ne pripada sprejeti domeni. Obrnite se na skrbnika ali se prijavite z drugim naslovom e-pošte." + }, + { + "id": "api.user.create_profile_image.initial.app_error", + "translation": "Ni mogoče dodati začetne slike uporabnika na privzeto sliko profila" + }, + { + "id": "api.user.create_profile_image.encode.app_error", + "translation": "Privzete slike profila ni mogoče kodirati" + }, + { + "id": "api.user.create_profile_image.default_font.app_error", + "translation": "Ni možno ustvariti privzete pisave za sliko profila" + }, + { + "id": "api.user.create_oauth_user.create.app_error", + "translation": "Uporabnika ni bilo mogoče ustvariti iz uporabniškega objekta {{.Service}}" + }, + { + "id": "api.user.create_oauth_user.already_attached.app_error", + "translation": "S tem e-poštnim naslovom že obstaja račun, ki uporablja način prijave, ki ni {{.Service}}. Prijavite se z uporabo {{.Auth}}." + }, + { + "id": "model.group.source.app_error", + "translation": "neveljavna lastnost vira za skupino" + }, + { + "id": "model.group.remote_id.app_error", + "translation": "neveljavna lastnost oddaljenega ID za skupino" + }, + { + "id": "model.group.name.app_error", + "translation": "neveljavno ime lastnosti za skupino" + }, + { + "id": "model.group.display_name.app_error", + "translation": "neveljavna lastnost prikaznega imena za skupino" + }, + { + "id": "model.group.description.app_error", + "translation": "neveljavna lastnost opisa za skupino" + }, + { + "id": "model.group.create_at.app_error", + "translation": "neveljavna posodobitev za lastnost za skupino" + }, + { + "id": "model.file_info.is_valid.user_id.app_error", + "translation": "Neveljavna vrednost ID uporabnika." + }, + { + "id": "model.file_info.is_valid.update_at.app_error", + "translation": "Neveljavna vrednost za update_at." + }, + { + "id": "model.file_info.is_valid.post_id.app_error", + "translation": "Neveljavna vrednost za ID objave." + }, + { + "id": "model.file_info.is_valid.path.app_error", + "translation": "Neveljavna vrednost za pot." + }, + { + "id": "model.file_info.is_valid.id.app_error", + "translation": "Neveljavna ID vrednost." + }, + { + "id": "model.file_info.is_valid.create_at.app_error", + "translation": "Neveljavna vrednost za create_at." + }, + { + "id": "model.file_info.get.gif.app_error", + "translation": "Gif datoteke ni mogoče dekodirati." + }, + { + "id": "model.emoji.user_id.app_error", + "translation": "Neveljaven ID ustvarjalca" + }, + { + "id": "model.emoji.update_at.app_error", + "translation": "Posodobi na mora biti veljaven čas" + }, + { + "id": "model.emoji.name.app_error", + "translation": "Ime mora vsebovati od 1 do 64 malih alfanumeričnih znakov" + }, + { + "id": "model.emoji.id.app_error", + "translation": "Neveljaven ID čustvenega simbola" + }, + { + "id": "model.emoji.create_at.app_error", + "translation": "Ustvari na mora biti veljaven čas" + }, + { + "id": "model.config.is_valid.write_timeout.app_error", + "translation": "Neveljavna vrednost za časovno omejitev pisanja." + }, + { + "id": "model.config.is_valid.websocket_url.app_error", + "translation": "URL spletne vtičnice mora biti veljaven URL in začeti z ws:// ali wss://" + }, + { + "id": "model.config.is_valid.webserver_security.app_error", + "translation": "Neveljavna vrednost za varnost povezave do spletnega strežnika." + }, + { + "id": "model.config.is_valid.time_between_user_typing.app_error", + "translation": "Čas med posodobitvami tipkanja uporabnikov ne sme biti manjši od 1000 milisekund." + }, + { + "id": "model.config.is_valid.teammate_name_display.app_error", + "translation": "Neveljaven prikaz za ekipo. Mora biti 'polno_ime', 'vzdevek_polno_ime' ali 'uporabniško ime'" + }, + { + "id": "model.config.is_valid.sql_query_timeout.app_error", + "translation": "Neveljavna časovna omejitev poizvedbe za nastavitve SQL. Mora biti pozitivno število." + }, + { + "id": "model.config.is_valid.sql_max_conn.app_error", + "translation": "Neveljavna največja odprta povezava za nastavitve SQL. Mora biti pozitivno število." + }, + { + "id": "model.config.is_valid.sql_idle.app_error", + "translation": "Neveljavna najvišja povezava v stanju pripravljenosti za nastavitve SQL. Mora biti pozitivno število." + }, + { + "id": "model.config.is_valid.sql_driver.app_error", + "translation": "Neveljavno ime gonilnika za nastavitve SQL. Mora biti »mysql« ali »postgres«" + }, + { + "id": "model.config.is_valid.sql_data_src.app_error", + "translation": "Neveljaven vir podatkov za nastavitve SQL. Mora biti nastavljen." + }, + { + "id": "model.config.is_valid.sql_conn_max_lifetime_milliseconds.app_error", + "translation": "Neveljavna življenjska doba povezave za nastavitve SQL. Mora biti negativno število." + }, + { + "id": "model.config.is_valid.sitename_length.app_error", + "translation": "Ime strani mora biti manjše ali enako {{.MaxLength}} znakov." + }, + { + "id": "model.config.is_valid.site_url_email_batching.app_error", + "translation": "Združevanje e-poštnih sporočil ni mogoče omogočiti, če ni nastavljen SiteURL." + }, + { + "id": "model.config.is_valid.site_url.app_error", + "translation": "Neveljaven URL. Mora biti veljaven URL in se začeti s http:// ali https://" + }, + { + "id": "model.config.is_valid.saml_username_attribute.app_error", + "translation": "Neveljaven atribut uporabniškega imena. Mora biti nastavljen." + }, + { + "id": "model.config.is_valid.saml_public_cert.app_error", + "translation": "Manjka javno potrdilo ponudnika storitev. Ali ste ga pozabili naložiti?" + }, + { + "id": "model.config.is_valid.saml_private_key.app_error", + "translation": "Manjka zasebni ključ ponudnika storitev. Ali ste ga pozabili naložiti?" + }, + { + "id": "model.config.is_valid.saml_idp_url.app_error", + "translation": "URL SAML SSO mora biti veljaven URL in začeti s http:// ali https://." + }, + { + "id": "model.config.is_valid.saml_idp_descriptor_url.app_error", + "translation": "URL izdajatelja identitete ponudnika mora biti veljaven URL in se začeti s http:// ali https://." + }, + { + "id": "model.config.is_valid.saml_idp_cert.app_error", + "translation": "Manjka javno potrdilo ponudnika identitete. Ali ste ga pozabili naložiti?" + }, + { + "id": "model.config.is_valid.saml_email_attribute.app_error", + "translation": "Neveljaven atribut e-pošte. Mora biti nastavljen." + }, + { + "id": "model.config.is_valid.saml_assertion_consumer_service_url.app_error", + "translation": "URL za prijavo ponudnika storitev mora biti veljaven URL in začeti s http:// ali https://." + }, + { + "id": "model.config.is_valid.restrict_direct_message.app_error", + "translation": "Neveljavna omejitev neposrednega sporočila. Mora biti »vsaka« ali »skupina«" + }, + { + "id": "model.config.is_valid.read_timeout.app_error", + "translation": "Neveljavna vrednost časovne omejitve branja." + }, + { + "id": "model.config.is_valid.rate_sec.app_error", + "translation": "Neveljavna velikost pomnilnika za nastavitve omejitve hitrosti. Mora biti pozitivno število" + }, + { + "id": "model.config.is_valid.rate_mem.app_error", + "translation": "Neveljavna velikost pomnilnika za nastavitve omejitve hitrosti. Mora biti pozitivno število" + }, + { + "id": "model.config.is_valid.password_length.app_error", + "translation": "Najmanjša dolžina gesla mora biti celo število, ki je večje ali enako {{.MinLength}} in je manjše ali enako {{.MaxLength}}." + }, + { + "id": "model.config.is_valid.max_users.app_error", + "translation": "Neveljavno najvišje število uporabnikov na ekipo za nastavitve ekipe. Mora biti pozitivno število." + }, + { + "id": "model.config.is_valid.max_notify_per_channel.app_error", + "translation": "Neveljavno najvišje število obvestil na kanal za nastavitve ekipe. Mora biti pozitivno število." + }, + { + "id": "model.config.is_valid.max_file_size.app_error", + "translation": "Neveljavna velikost datoteke za nastavitve datotek. Mora biti celo število, ki je večje od nič." + }, + { + "id": "model.config.is_valid.max_channels.app_error", + "translation": "Neveljavno najvišje število kanalov na ekipo za nastavitve ekipe. Mora biti pozitivno število." + }, + { + "id": "model.config.is_valid.login_attempts.app_error", + "translation": "Neveljavno najvišje število uporabnikov na ekipo za nastavitve ekipe. Mora biti pozitivno število." + }, + { + "id": "model.config.is_valid.localization.available_locales.app_error", + "translation": "Razpoložljivi jeziki morajo vsebovati privzeti jezik odjemalca" + }, + { + "id": "model.config.is_valid.ldap_username", + "translation": "Polje AD / LDAP \"Atribut uporabniško ime\" je obvezno." + }, + { + "id": "model.config.is_valid.ldap_sync_interval.app_error", + "translation": "Neveljaven časovni interval sinhronizacije. Mora biti vsaj eno minuto." + }, + { + "id": "model.config.is_valid.ldap_server", + "translation": "AD / LDAP polje \"AD / LDAP strežnik\" je obvezno." + }, + { + "id": "model.config.is_valid.ldap_security.app_error", + "translation": "Neveljavna varnost povezave za nastavitve AD/LDAP. Mora biti '', 'TLS' ali 'STARTTLS'" + }, + { + "id": "model.config.is_valid.ldap_max_page_size.app_error", + "translation": "Neveljavna največja velikost strani." + }, + { + "id": "model.config.is_valid.ldap_login_id", + "translation": "Polje AD / LDAP \"Atribut uporabniško ime\" je obvezno." + }, + { + "id": "model.config.is_valid.ldap_id", + "translation": "Polje AD / LDAP \"Atribut uporabniško ime\" je obvezno." + }, + { + "id": "model.config.is_valid.ldap_email", + "translation": "Polje AD / LDAP \"Atribut uporabniško ime\" je obvezno." + }, + { + "id": "model.config.is_valid.ldap_basedn", + "translation": "AD/LDAP polje \"BaseDN\" je obvezno." + }, + { + "id": "model.config.is_valid.group_unread_channels.app_error", + "translation": "Nepravilni neprebrani kanali za nastavitve storitev. Mora biti 'disabled', 'default_on' ali 'default_off'." + }, + { + "id": "model.config.is_valid.file_salt.app_error", + "translation": "Neveljaven salt za povabila za nastavitve e-pošte. Mora biti sestavljen iz 32 znakov ali več." + }, + { + "id": "model.config.is_valid.file_driver.app_error", + "translation": "Neveljavno ime gonilnika za nastavitve datoteke. Mora biti »lokalno« ali »amazons3«" + }, + { + "id": "model.config.is_valid.email_security.app_error", + "translation": "Neveljavna varnost povezave za nastavitve e-pošte. Mora biti '', 'TLS' ali 'STARTTLS'" + }, + { + "id": "model.config.is_valid.display.custom_url_schemes.app_error", + "translation": "Shema URL-ja po meri {{.Scheme}} je neveljavna. Sheme URL-jev po meri se morajo začeti s črko in vsebovati le črke, številke in vezaj (-)." + }, + { + "id": "model.config.is_valid.data_retention.message_retention_days_too_low.app_error", + "translation": "Hramba sporočila mora biti en dan ali več." + }, + { + "id": "model.config.is_valid.data_retention.file_retention_days_too_low.app_error", + "translation": "Hranjenje datotek mora biti en dan ali več." + }, + { + "id": "model.config.is_valid.data_retention.deletion_job_start_time.app_error", + "translation": "Čas začetka dela za hrambo podatkov mora biti 24-urni časovni žig v obliki HH: MM." + }, + { + "id": "model.config.is_valid.cluster_email_batching.app_error", + "translation": "Ko je združevanje v gruče omogočeno, ni mogoče omogočiti pošiljanja po e-pošti." + }, + { + "id": "model.config.is_valid.allow_cookies_for_subdomains.app_error", + "translation": "Če dovolite piškotke za poddomene, morate nastaviti SiteURL." + }, + { + "id": "model.compliance.is_valid.id.app_error", + "translation": "Neveljaven ID" + }, + { + "id": "model.compliance.is_valid.desc.app_error", + "translation": "Neveljaven opis" + }, + { + "id": "model.compliance.is_valid.create_at.app_error", + "translation": "Ustvari na mora biti veljaven čas" + }, + { + "id": "model.command_hook.user_id.app_error", + "translation": "Neveljaven ID uporabnika." + }, + { + "id": "model.command_hook.root_id.app_error", + "translation": "Neveljaven root ID" + }, + { + "id": "model.command_hook.id.app_error", + "translation": "Neveljaven ID ukaza" + }, + { + "id": "model.command_hook.create_at.app_error", + "translation": "Ustvari na mora biti veljaven čas" + }, + { + "id": "model.command_hook.command_id.app_error", + "translation": "Neveljaven ID ukaza" + }, + { + "id": "model.command_hook.channel_id.app_error", + "translation": "Neveljaven ID kanala" + }, + { + "id": "model.command.is_valid.user_id.app_error", + "translation": "Neveljaven ID uporabnika." + }, + { + "id": "model.command.is_valid.url_http.app_error", + "translation": "Neveljaven URL. Mora biti veljaven URL in se začeti s http: // ali https: //" + }, + { + "id": "model.command.is_valid.url.app_error", + "translation": "Neveljaven URL" + }, + { + "id": "model.command.is_valid.update_at.app_error", + "translation": "Posodobi na mora biti veljaven čas" + }, + { + "id": "model.command.is_valid.trigger.app_error", + "translation": "Neveljaven sprožilec" + }, + { + "id": "model.command.is_valid.token.app_error", + "translation": "Neveljaven žeton." + }, + { + "id": "model.command.is_valid.team_id.app_error", + "translation": "Neveljaven ID ekipe" + }, + { + "id": "model.command.is_valid.method.app_error", + "translation": "Neveljavna metoda" + }, + { + "id": "model.command.is_valid.id.app_error", + "translation": "Neveljaven ID" + }, + { + "id": "model.command.is_valid.display_name.app_error", + "translation": "Neveljaven naslov" + }, + { + "id": "model.command.is_valid.description.app_error", + "translation": "Neveljaven opis" + }, + { + "id": "model.command.is_valid.create_at.app_error", + "translation": "Ustvari na mora biti veljaven čas" + }, + { + "id": "model.cluster.is_valid.type.app_error", + "translation": "Tip mora biti nastavljen" + }, + { + "id": "model.cluster.is_valid.name.app_error", + "translation": "ClusterName mora biti nastavljen" + }, + { + "id": "model.cluster.is_valid.last_ping_at.app_error", + "translation": "LastPingAt mora biti nastavljen" + }, + { + "id": "model.cluster.is_valid.id.app_error", + "translation": "Neveljaven ID" + }, + { + "id": "model.cluster.is_valid.hostname.app_error", + "translation": "Hostname mora biti nastavljen" + }, + { + "id": "model.cluster.is_valid.create_at.app_error", + "translation": "CreateAt mora biti nastavljen" + }, + { + "id": "model.channel_member.is_valid.user_id.app_error", + "translation": "Neveljaven ID uporabnika." + }, + { + "id": "model.channel_member.is_valid.unread_level.app_error", + "translation": "Neveljaven označite neprebrano raven" + }, + { + "id": "model.channel_member.is_valid.push_level.app_error", + "translation": "Neveljavna raven potisnega obvestila" + }, + { + "id": "model.channel_member.is_valid.notify_level.app_error", + "translation": "Neveljavna raven obveščanja" + }, + { + "id": "model.channel_member.is_valid.email_value.app_error", + "translation": "Neveljavna vrednost e-poštnega obvestila" + }, + { + "id": "model.channel_member.is_valid.channel_id.app_error", + "translation": "Neveljaven ID kanala" + }, + { + "id": "model.channel.is_valid.update_at.app_error", + "translation": "Posodobi na mora biti veljaven čas" + }, + { + "id": "model.channel.is_valid.type.app_error", + "translation": "Neveljavna vrsta" + }, + { + "id": "model.channel.is_valid.purpose.app_error", + "translation": "Neveljavni namen" + }, + { + "id": "model.channel.is_valid.id.app_error", + "translation": "Neveljaven ID" + }, + { + "id": "model.channel.is_valid.header.app_error", + "translation": "Neveljavna glava" + }, + { + "id": "model.channel.is_valid.display_name.app_error", + "translation": "Neveljavno ime za prikaz" + }, + { + "id": "model.channel.is_valid.creator_id.app_error", + "translation": "Neveljaven ID ustvarjalca" + }, + { + "id": "model.channel.is_valid.create_at.app_error", + "translation": "Ustvari na mora biti veljaven čas" + }, + { + "id": "model.bot.is_valid.username.app_error", + "translation": "Neveljavno uporabniško ime." + }, + { + "id": "model.bot.is_valid.user_id.app_error", + "translation": "Neveljaven ID uporabnika." + }, + { + "id": "model.bot.is_valid.update_at.app_error", + "translation": "Neveljaven ID ustvarjalca" + }, + { + "id": "model.bot.is_valid.description.app_error", + "translation": "Neveljaven opis" + }, + { + "id": "model.bot.is_valid.creator_id.app_error", + "translation": "Neveljaven ID ustvarjalca" + }, + { + "id": "model.bot.is_valid.create_at.app_error", + "translation": "Neveljaven ID ustvarjalca" + }, + { + "id": "model.authorize.is_valid.user_id.app_error", + "translation": "Neveljaven ID uporabnika." + }, + { + "id": "model.authorize.is_valid.state.app_error", + "translation": "Neveljavno stanje" + }, + { + "id": "model.authorize.is_valid.scope.app_error", + "translation": "Neveljaven obseg" + }, + { + "id": "model.authorize.is_valid.response_type.app_error", + "translation": "Neveljavna vrsta odgovora" + }, + { + "id": "model.authorize.is_valid.redirect_uri.app_error", + "translation": "Neveljaven preusmeritveni uri" + }, + { + "id": "model.authorize.is_valid.expires.app_error", + "translation": "Poteče v mora biti nastavljen" + }, + { + "id": "model.authorize.is_valid.create_at.app_error", + "translation": "Ustvari na mora biti veljaven čas" + }, + { + "id": "model.authorize.is_valid.client_id.app_error", + "translation": "Neveljaven ID odjemalca" + }, + { + "id": "model.authorize.is_valid.auth_code.app_error", + "translation": "Neveljavna avtorizacijska koda" + }, + { + "id": "model.access.is_valid.user_id.app_error", + "translation": "Neveljaven ID uporabnika." + }, + { + "id": "model.access.is_valid.refresh_token.app_error", + "translation": "Neveljaven žeton osveževanja" + }, + { + "id": "model.access.is_valid.redirect_uri.app_error", + "translation": "Neveljaven preusmeritveni uri" + }, + { + "id": "model.access.is_valid.client_id.app_error", + "translation": "Neveljaven ID odjemalca" + }, + { + "id": "model.access.is_valid.access_token.app_error", + "translation": "Neveljaven žeton za dostop" + }, + { + "id": "migrations.worker.run_migration.unknown_key", + "translation": "Opravila migracije ni mogoče zagnati zaradi neznanega ključa za migracijo." + }, + { + "id": "api.user.complete_switch_with_oauth.parse.app_error", + "translation": "Podatkov za avtorizacijo ni bilo mogoče razčleniti iz uporabniškega objekta {{.Service}}" + }, + { + "id": "api.user.complete_switch_with_oauth.blank_email.app_error", + "translation": "Prazna e-pošta" + }, + { + "id": "api.user.check_user_password.invalid.app_error", + "translation": "Prijava ni uspela zaradi neveljavnega gesla" + }, + { + "id": "api.user.check_user_mfa.bad_code.app_error", + "translation": "Neveljaven MFA žeton." + }, + { + "id": "api.user.check_user_login_attempts.too_many.app_error", + "translation": "Vaš račun je zaklenjen zaradi preveč poskusov neuspešnega gesla. Ponastavite geslo." + }, + { + "id": "api.user.authorize_oauth_user.unsupported.app_error", + "translation": "Ponudnik storitve OAuth ni podprt" + }, + { + "id": "api.user.authorize_oauth_user.token_failed.app_error", + "translation": "Zahteva za žeton ni uspela" + }, + { + "id": "api.user.authorize_oauth_user.service.app_error", + "translation": "Zahteva za žeton do {{.Service}} ni uspela" + }, + { + "id": "api.user.authorize_oauth_user.response.app_error", + "translation": "Ponudnik storitve OAuth je poslal neveljaven odgovor" + }, + { + "id": "api.user.authorize_oauth_user.missing.app_error", + "translation": "Manjka žeton za dostop" + }, + { + "id": "api.user.authorize_oauth_user.invalid_state.app_error", + "translation": "Neveljavno stanje" + }, + { + "id": "api.user.authorize_oauth_user.bad_token.app_error", + "translation": "Slab tip žetona" + }, + { + "id": "api.user.authorize_oauth_user.bad_response.app_error", + "translation": "Napačen odgovor na zahtevo za žeton" + }, + { + "id": "api.user.add_direct_channels_and_forget.failed.error", + "translation": "Nastavitev neposrednega kanala za uporabnika user_id={{. UserId}}, team_id={{. TeamId}}, err={{.Error}}" + }, + { + "id": "api.user.activate_mfa.email_and_ldap_only.app_error", + "translation": "MFA ni na voljo za to vrsto računa" + }, + { + "id": "api.templates.welcome_subject", + "translation": "[{{.SiteName}}] Pridružili ste se {{.ServerURL}}" + }, + { + "id": "api.templates.welcome_body.title", + "translation": "Pridružili ste se {{.ServerURL}}" + }, + { + "id": "api.templates.welcome_body.info", + "translation": "Potrdite svoj e-poštni naslov tako, da kliknete spodaj." + }, + { + "id": "api.templates.welcome_body.button", + "translation": "Potrdite vašo e-pošto" + }, + { + "id": "api.templates.welcome_body.app_download_info", + "translation": "Za najboljšo izkušnjo prenesite aplikacije za PC, Mac, iOS in Android iz:" + }, + { + "id": "api.templates.verify_subject", + "translation": "[{{.SiteName}}] Potrjevanje e-pošte" + }, + { + "id": "api.templates.verify_body.title", + "translation": "Pridružili ste se {{.ServerURL}}" + }, + { + "id": "api.templates.verify_body.info", + "translation": "Potrdite svoj e-poštni naslov tako, da kliknete spodaj." + }, + { + "id": "api.templates.verify_body.button", + "translation": "Potrdite vašo e-pošto" + }, + { + "id": "api.templates.username_change_subject", + "translation": "[{{.SiteName}}] Vaše uporabniško ime je bilo spremenjeno" + }, + { + "id": "api.templates.username_change_body.title", + "translation": "Posodobili ste uporabniško ime" + }, + { + "id": "api.templates.username_change_body.info", + "translation": "Vaše uporabniško ime za {{.TeamDisplayName}} je bilo spremenjeno v {{.NewUsername}}." + }, + { + "id": "api.templates.user_access_token_subject", + "translation": "[{{.SiteName}}] Osebni dostopni žeton je dodan v vaš račun" + }, + { + "id": "api.templates.user_access_token_body.title", + "translation": "V račun je dodan žeton za osebni dostop" + }, + { + "id": "api.templates.user_access_token_body.info", + "translation": "Na račun {{.SiteURL}} je bil dodan žeton za osebni dostop. Uporabite ga lahko za dostop do {{.SiteName}} z vašim računom." + }, + { + "id": "api.templates.signin_change_email.subject", + "translation": "[{{.SiteName}}] Način prijave je bila posodobljen" + }, + { + "id": "api.templates.signin_change_email.body.title", + "translation": "Posodobili ste način prijave" + }, + { + "id": "api.templates.signin_change_email.body.method_email", + "translation": "e-pošta in geslo" + }, + { + "id": "api.templates.signin_change_email.body.info", + "translation": "Svojo metodo za prijavo na {{.SiteName}} ste posodobili na {{.Method}}." + }, + { + "id": "api.templates.reset_subject", + "translation": "[{{.SiteName}}] Ponastavite geslo" + }, + { + "id": "api.templates.reset_body.title", + "translation": "Zahtevali ste ponastavitev gesla" + }, + { + "id": "api.templates.reset_body.button", + "translation": "Ponastavite geslo" + }, + { + "id": "api.templates.post_body.button", + "translation": "Pojdi na objavo" + }, + { + "id": "api.templates.password_change_subject", + "translation": "[{{ .SiteName }}] Vaše geslo je bilo spremenjeno" + }, + { + "id": "api.templates.password_change_body.title", + "translation": "Vaše geslo je bilo spremenjeno" + }, + { + "id": "api.templates.password_change_body.info", + "translation": "Vaše geslo je bilo posodobljeno za {{.TeamDisplayName}} na {{.TeamURL}} od {{.Method}}." + }, + { + "id": "api.templates.mfa_deactivated_body.title", + "translation": "Več faktorsko preverjanje pristnosti je bilo odstranjeno" + }, + { + "id": "api.templates.mfa_deactivated_body.info", + "translation": "Več faktorsko preverjanje pristnosti je bilo odstranjeno iz vašega računa na {{.SiteURL}}." + }, + { + "id": "api.templates.mfa_change_subject", + "translation": "[{{.SiteName}}] Vaša MFA je bila posodobljena" + }, + { + "id": "api.templates.mfa_activated_body.title", + "translation": "Dodano je bilo več faktorsko preverjanje pristnosti" + }, + { + "id": "api.templates.mfa_activated_body.info", + "translation": "Na račun {{.SiteURL}} je bilo dodano več faktorsko preverjanje pristnosti." + }, + { + "id": "api.templates.invite_subject", + "translation": "[{{.SiteName}}] {{.SenderName}} vas je povabil, da se pridružite ekipi {{.TeamDisplayName}}" + }, + { + "id": "api.templates.invite_guest_subject", + "translation": "[{{.SiteName}}] {{.SenderName}} vas je povabil, da se pridružite ekipi {{.TeamDisplayName}}" + }, + { + "id": "api.templates.invite_body.title", + "translation": "Bili ste povabljeni" + }, + { + "id": "api.templates.invite_body.button", + "translation": "Pridruži se ekipi" + }, + { + "id": "api.templates.email_warning", + "translation": "Če niste storili te spremembe, prosim kontaktirajte skrbnika sistema." + }, + { + "id": "api.templates.email_organization", + "translation": "Poslano od" + }, + { + "id": "api.templates.email_info3", + "translation": "{{.SiteName}} ekipa" + }, + { + "id": "api.templates.email_info2", + "translation": "Z lepimi pozdravi," + }, + { + "id": "api.templates.email_info1", + "translation": "Za kakršnakoli vprašanja, nam pošljite kadarkoli sporočilo:" + }, + { + "id": "api.templates.email_footer", + "translation": "Če želite spremeniti nastavitve obvestil, se prijavite na spletno mesto svoje ekipe in odprite Nastavitve računa > Obvestila." + }, + { + "id": "api.templates.email_change_verify_subject", + "translation": "[{{.SiteName}}] Preverite novi e-poštni naslov" + }, + { + "id": "api.templates.email_change_verify_body.title", + "translation": "Spremenili ste vašo e-pošto" + }, + { + "id": "api.templates.email_change_verify_body.info", + "translation": "Če želite dokončati posodabljanje e-poštnega naslova za {{.TeamDisplayName}}, kliknite spodnjo povezavo, da potrdite, da je to pravi naslov." + }, + { + "id": "api.templates.email_change_verify_body.button", + "translation": "Potrdite vašo e-pošto" + }, + { + "id": "api.templates.email_change_subject", + "translation": "[{{ .SiteName }}] Vaša e-pošta je bila spremenjena" + }, + { + "id": "api.templates.email_change_body.title", + "translation": "Spremenili ste vašo e-pošto" + }, + { + "id": "api.templates.email_change_body.info", + "translation": "Vaš e-poštni naslov za {{.TeamDisplayName}} je bil spremenjen v {{.NewEmail}}." + }, + { + "id": "api.templates.deactivate_subject", + "translation": "[{{ .SiteName }}] Vaš uporabniški račun na {{ .ServerURL }} je bil onemogočen" + }, + { + "id": "api.templates.deactivate_body.warning", + "translation": "Če ta sprememba ni bila storjena z vaše strani ali če želite ponvono omogočiti vaš uporabniški račun, prosim kontaktirajte vašega skrbnika sistema." + }, + { + "id": "api.templates.deactivate_body.title", + "translation": "Vaš uporabniški račun je bil onemogočen na {{ .ServerURL }}" + }, + { + "id": "api.templates.deactivate_body.info", + "translation": "Onemogočili ste vaš uporabniški račun na {{ .SiteURL }}." + }, + { + "id": "api.team.update_team_scheme.scheme_scope.error", + "translation": "Sheme ni mogoče nastaviti za ekipo, ker priložena shema ni ekipna shema." + }, + { + "id": "api.team.update_team_scheme.license.error", + "translation": "Vaša licenca ne podpira posodabljanja sheme ekipe" + }, + { + "id": "api.team.update_restricted_domains.mismatch.app_error", + "translation": "Sistemska konfiguracija ne dovoljuje omejevanja skupine na {{.Domain}}. Obrnite se na skrbnika sistema." + }, + { + "id": "api.team.update_member_roles.not_a_member", + "translation": "Navedeni uporabnik ni član določene ekipe." + }, + { + "id": "api.team.team_icon.update.app_error", + "translation": "Pri posodabljanju ikone skupine je prišlo do napake" + }, + { + "id": "api.team.set_team_icon.write_file.app_error", + "translation": "Ikone skupine ni bilo mogoče shraniti" + }, + { + "id": "api.team.set_team_icon.too_large.app_error", + "translation": "Ikono ekipe ni mogoče naložiti. Datoteka je prevelika." + }, + { + "id": "api.team.set_team_icon.storage.app_error", + "translation": "Ikone ekipe ni mogoče naložiti. Shramba slik ni konfigurirana." + }, + { + "id": "api.team.set_team_icon.parse.app_error", + "translation": "Ni mogoče prebrati multipart obrazca" + }, + { + "id": "api.team.set_team_icon.open.app_error", + "translation": "Slikovne datoteke ni bilo mogoče odpreti" + }, + { + "id": "api.team.set_team_icon.no_file.app_error", + "translation": "Ni datoteke v polju 'slika' v zahtevi" + }, + { + "id": "api.team.set_team_icon.get_team.app_error", + "translation": "Pri pridobivanju ekipe je prišlo do napake" + }, + { + "id": "api.team.set_team_icon.encode.app_error", + "translation": "Ikone skupine ni bilo mogoče kodirati" + }, + { + "id": "api.team.set_team_icon.decode.app_error", + "translation": "Ikone skupine ni bilo mogoče dekodirati" + }, + { + "id": "api.team.set_team_icon.array.app_error", + "translation": "Prazen seznam v polju 'image' v zahtevi" + }, + { + "id": "api.team.remove_user_from_team.removed", + "translation": "%v je bil odstranjen iz ekipe." + }, + { + "id": "api.team.remove_user_from_team.missing.app_error", + "translation": "Uporabnik ni del ekipe." + }, + { + "id": "api.team.remove_team_icon.get_team.app_error", + "translation": "Pri pridobivanju ekipe je prišlo do napake" + }, + { + "id": "api.team.move_channel.success", + "translation": "Ta kanal je bil premaknjen v to skupino iz %v." + }, + { + "id": "api.team.move_channel.post.error", + "translation": "Sporočila o premikanju kanala ni bilo mogoče objaviti." + }, + { + "id": "api.team.leave.left", + "translation": "%v je zapustil ekipo." + }, + { + "id": "api.team.join_team.post_and_forget", + "translation": "%v se je pridružil ekipi." + }, + { + "id": "api.team.is_team_creation_allowed.disabled.app_error", + "translation": "Ustvarjanje ekipe je bilo onemogočeno. Za podrobnosti se obrnite na skrbnika sistema." + }, + { + "id": "api.team.invite_members.no_one.app_error", + "translation": "Ne najdem nikogar za pošiljanje povabila." + }, + { + "id": "api.team.invite_members.invalid_email.app_error", + "translation": "Naslednji e-poštni naslovi ne pripadajo sprejeti domeni: {{.Addresses}}. Za podrobnosti se obrnite na skrbnika sistema." + }, + { + "id": "api.team.invite_members.disabled.app_error", + "translation": "Vabila po e-pošti so onemogočena." + }, + { + "id": "api.team.import_team.unavailable.app_error", + "translation": "Okvarjena zahteva: polje velikost datoteke ne obstaja." + }, + { + "id": "api.team.import_team.parse.app_error", + "translation": "Ni mogoče prebrati multipart obrazca" + }, + { + "id": "api.team.import_team.open.app_error", + "translation": "Datoteke ni bilo mogoče odpreti" + }, + { + "id": "api.team.import_team.no_import_from.app_error", + "translation": "Napačna zahteva: polje importFrom ni prisotno." + }, + { + "id": "api.slackimport.slack_import.notes", + "translation": "\r\nZapiski:\r\n" + }, + { + "id": "api.slackimport.slack_import.note3", + "translation": "- V strežniških zapisih se lahko najdejo dodatne napake.\r\n" + }, + { + "id": "api.slackimport.slack_import.note2", + "translation": "- Slack bot sporočila trenutno niso podprta.\r\n" + }, + { + "id": "api.slackimport.slack_import.note1", + "translation": "- Nekatera sporočila morda niso bila uvožena, ker jih ta uvoznik ni podprl.\r\n" + }, + { + "id": "api.slackimport.slack_import.log", + "translation": "Mattermost Slack dnevnik uvoza\r\n" + }, + { + "id": "api.slackimport.slack_add_users.unable_import", + "translation": "Ni možno uvoziti Slack uporabnika: {{.Username}}.\r\n" + }, + { + "id": "api.slackimport.slack_add_users.missing_email_address", + "translation": "Uporabnik {{.Username}} nima e-poštnega naslova v Slack izvozu. Uporabljen je {{.Email}} kot placeholder. Uporabnik mora posodobiti svoj e-poštni naslov, ko se bo prijavil v sistem.\r\n" + }, + { + "id": "api.slackimport.slack_add_users.merge_existing_failed", + "translation": "Slack uporabnik je združen z obstoječim Mattermost uporabnikom z ujemajočo e-pošto {{.Email}} in uporabniškim imenom {{.Username}}, vendar ni bilo možno dodati uporabnika v njegovo ekipo.\r\n" + }, + { + "id": "api.slackimport.slack_add_users.merge_existing", + "translation": "Slack uporabnik je združen z obstoječim Mattermost uporabnikom z ujemajočo e-pošto {{.Email}} in uporabniškim imenom {{.Username}}.\r\n" + }, + { + "id": "api.slackimport.slack_add_users.email_pwd", + "translation": "Slack uporabnik s sledečo e-pošto {{.Email}} in geslom {{.Password}} je bil uvožen.\r\n" + }, + { + "id": "api.slackimport.slack_add_users.created", + "translation": "\r\nUstvarjeni uporabniki:\r\n" + }, + { + "id": "api.slackimport.slack_add_channels.merge", + "translation": "Slack kanal {{.DisplayName}} že obstaja kot aktiven Mattermost kanal. Oba kanala sta sedaj združena.\r\n" + }, + { + "id": "api.slackimport.slack_add_channels.import_failed", + "translation": "Ni možno uvoziti Slack kanala {{.DisplayName}}.\r\n" + }, + { + "id": "api.slackimport.slack_add_channels.failed_to_add_user", + "translation": "Ni možno dodati Slack uporabnika {{.Username}} v kanal.\r\n" + }, + { + "id": "api.slackimport.slack_add_channels.added", + "translation": "\r\nDodani kanali:\r\n" + }, + { + "id": "api.slackimport.slack_add_bot_user.unable_import", + "translation": "Ne morem uvoziti uporabnika Integration/Slack Bot {{.User username}}.\r\n" + }, + { + "id": "api.slackimport.slack_add_bot_user.email_pwd", + "translation": "Slack uporabnik s sledečo e-pošto {{.Email}} in geslom {{.Password}} je bil uvožen.\r\n" + }, + { + "id": "api.server.start_server.starting.critical", + "translation": "Napaka pri zagonu strežnika, napaka:%v" + }, + { + "id": "api.server.start_server.rate_limiting_rate_limiter", + "translation": "Omejitve hitrosti ni mogoče inicializirati." + }, + { + "id": "api.server.start_server.rate_limiting_memory_store", + "translation": "Shrambe pomnilnika za omejevanje hitrosti ni mogoče inicializirati. Preverite nastavitve konfiguracije MemoryStoreSize." + }, + { + "id": "api.server.start_server.forward80to443.enabled_but_listening_on_wrong_port", + "translation": "Vrata 80 ni mogoče posredovati v vrata 443 med poslušanjem vrat %s: onemogočite funkcijo Forward80To443, če uporabljate proxy strežnik" + }, + { + "id": "api.server.start_server.forward80to443.disabled_while_using_lets_encrypt", + "translation": "Ko uporabljate funkcijo LetsEncrypt, morate omogočiti funkcijo Forward80To443" + }, + { + "id": "api.scheme.patch_scheme.license.error", + "translation": "Vaša licenca ne podpira shem dovoljenj za posodobitve" + }, + { + "id": "api.scheme.get_teams_for_scheme.scope.error", + "translation": "Ni mogoče dobiti ekip za shemo, ker podana shema ni ekipna shema." + }, + { + "id": "api.scheme.get_channels_for_scheme.scope.error", + "translation": "Ni mogoče dobiti kanalov za shemo, ker podana shema ni kanalna shema." + }, + { + "id": "api.scheme.delete_scheme.license.error", + "translation": "Vaša licenca ne podpira shem dovoljenj za brisanje" + }, + { + "id": "api.scheme.create_scheme.license.error", + "translation": "Vaša licenca ne podpira nadgradnjo sheme kanala." + }, + { + "id": "api.roles.patch_roles.license.error", + "translation": "Vaša licenca ne podpira nadgradnjo sheme kanala." + }, + { + "id": "api.reaction.save_reaction.user_id.app_error", + "translation": "Reakcije ne morete shraniti za drugega uporabnika." + }, + { + "id": "api.reaction.save_reaction.invalid.app_error", + "translation": "Reakcija ni veljavna." + }, + { + "id": "api.reaction.save.archived_channel.app_error", + "translation": "V arhiviranem kanalu ne morete reagirati." + }, + { + "id": "api.reaction.delete.archived_channel.app_error", + "translation": "Reakcije ne morete odstraniti v arhiviranem kanalu." + }, + { + "id": "api.preference.update_preferences.set.app_error", + "translation": "Ni možno nastaviti uporabniških nastavitev." + }, + { + "id": "api.preference.preferences_category.get.app_error", + "translation": "Ni možno pridobiti uporabnikovih nastavitev." + }, + { + "id": "api.preference.delete_preferences.delete.app_error", + "translation": "Ni možno izbrisati uporabnikovih nastavitev." + }, + { + "id": "api.post_get_post_by_id.get.app_error", + "translation": "Objave ni mogoče dobiti" + }, + { + "id": "api.post.update_post.system_message.app_error", + "translation": "Sistemskega sporočila ni mogoče posodobiti" + }, + { + "id": "api.post.update_post.permissions_time_limit.app_error", + "translation": "Urejanje objave je dovoljeno samo za {{.timeLimit}} sekund. Za podrobnosti se obrnite na skrbnika sistema." + }, + { + "id": "api.post.update_post.permissions_details.app_error", + "translation": "Že izbrisan id={{. PostId}}" + }, + { + "id": "api.post.update_post.find.app_error", + "translation": "Obstoječe objave ali komentarja za posodobitev ni mogoče najti." + }, + { + "id": "api.post.send_notifications_and_forget.push_message", + "translation": "ti je poslal sporočilo." + }, + { + "id": "api.post.send_notifications_and_forget.push_image_only", + "translation": " je priložil datoteko." + }, + { + "id": "api.post.send_notifications_and_forget.push_general_message", + "translation": " je objavil sporočilo." + }, + { + "id": "api.post.send_notifications_and_forget.push_explicit_mention", + "translation": " vas je omenil." + }, + { + "id": "api.post.send_notification_and_forget.push_comment_on_thread", + "translation": " je komentiral nit, v kateri ste sodelovali." + }, + { + "id": "api.post.send_notification_and_forget.push_comment_on_post", + "translation": " je komentiral vašo objavo." + }, + { + "id": "api.post.send_notification_and_forget.push_channel_mention", + "translation": " obvestil kanal." + }, + { + "id": "api.post.link_preview_disabled.app_error", + "translation": "Povzetek v emailu je bil izklopljen s strani administratorja." + }, + { + "id": "api.post.get_message_for_notification.images_sent", + "translation": { + "few": "", + "one": "{{.Count}} poslana slika: {{.Filenames}}", + "other": "", + "two": "{{.Count}} poslanih slik: {{.Filenames}}" + } + }, + { + "id": "api.post.get_message_for_notification.files_sent", + "translation": { + "few": "", + "one": "{{.Count}} poslana datoteka: {{.Filenames}}", + "other": "", + "two": "{{.Count}} poslanih datotek: {{.Filenames}}" + } + }, + { + "id": "api.post.do_action.action_integration.app_error", + "translation": "Napaka pri integraciji akcije" + }, + { + "id": "api.post.do_action.action_id.app_error", + "translation": "Neveljaven ID akcije" + }, + { + "id": "api.post.disabled_here", + "translation": "@here je onemogočen, ker ima kanal več kot {{.Users}} uporabnikov." + }, + { + "id": "api.post.disabled_channel", + "translation": "@channel je onemogočen, ker ima kanal več kot {{.Users}} uporabnikov." + }, + { + "id": "api.post.disabled_all", + "translation": "@all je onemogočen, ker ima kanal več kot {{.Users}} uporabnikov." + }, + { + "id": "api.post.deduplicate_create_post.pending", + "translation": "Objava je zavrnjena, ker drugi odjemalec dela isto zahtevo." + }, + { + "id": "api.post.deduplicate_create_post.failed_to_get", + "translation": "Prvotne objave ni bilo mogoče prenesti po podvajanju odjemalca, ki ponavlja isto zahtevo." + }, + { + "id": "api.post.create_webhook_post.creating.app_error", + "translation": "Napaka pri ustvarjanju objave" + }, + { + "id": "api.post.create_post.root_id.app_error", + "translation": "Neveljaven RootId parameter" + }, + { + "id": "api.post.create_post.channel_root_id.app_error", + "translation": "Neveljaven ChannelId za parameter RootId" + }, + { + "id": "api.post.create_post.can_not_post_to_deleted.error", + "translation": "Ni mogoče objaviti v izbrisanem kanalu." + }, + { + "id": "api.plugin.upload.no_file.app_error", + "translation": "Manjkajoča datoteka v večnivojski/formi zahtevi" + }, + { + "id": "api.plugin.upload.file.app_error", + "translation": "Datoteke ni mogoče odpreti v večnivojski/formi zahtevi" + }, + { + "id": "api.plugin.upload.array.app_error", + "translation": "Datotečni niz je prazen v večnivojski/formi zahtevi" + }, + { + "id": "api.outgoing_webhook.disabled.app_error", + "translation": "Administrator je onemogočil odhodne webhooks." + }, + { + "id": "api.oauth.singup_with_oauth.invalid_link.app_error", + "translation": "Prijavna povezava ni veljavna" + }, + { + "id": "api.oauth.singup_with_oauth.expired_link.app_error", + "translation": "Povezava za prijavo je potekla" + }, + { + "id": "api.oauth.singup_with_oauth.disabled.app_error", + "translation": "Prijava uporabnika je onemogočena." + }, + { + "id": "api.oauth.revoke_access_token.get.app_error", + "translation": "Napaka pri pridobivanju dostopnega žetona iz podatkovne zbirke pred brisanjem" + }, + { + "id": "api.oauth.revoke_access_token.del_token.app_error", + "translation": "Napaka pri brisanju žetona za dostop iz podatkovne zbirke" + }, + { + "id": "api.oauth.revoke_access_token.del_session.app_error", + "translation": "Napaka pri brisanju seje iz podatkovne zbirke" + }, + { + "id": "api.oauth.register_oauth_app.turn_off.app_error", + "translation": "Skrbnik sistema je izklopil ponudnika storitev OAuth2." + }, + { + "id": "api.oauth.invalid_state_token.app_error", + "translation": "Neveljaven žeton stanja" + }, + { + "id": "api.oauth.get_access_token.refresh_token.app_error", + "translation": "invalid_grant: Neveljaven žeton osveževanja" + }, + { + "id": "api.oauth.get_access_token.redirect_uri.app_error", + "translation": "invalid_request: Priložen redirect_uri se ne ujema z avtorizacijsko kodo redirect_uri" + }, + { + "id": "api.oauth.get_access_token.missing_refresh_token.app_error", + "translation": "invalid_request: Manjkajoči refresh_token" + }, + { + "id": "api.oauth.get_access_token.missing_code.app_error", + "translation": "invalid_request: Manjkajoča koda" + }, + { + "id": "api.oauth.get_access_token.internal_user.app_error", + "translation": "server_error: Prišlo je do notranje napake strežnika pri vlečenju uporabnika iz baze podatkov" + }, + { + "id": "api.oauth.get_access_token.internal_session.app_error", + "translation": "server_error: Med shranjevanjem seje v bazo podatkov je prišlo do notranje napake strežnika" + }, + { + "id": "api.oauth.get_access_token.internal_saving.app_error", + "translation": "server_error: Prišlo je do notranje napake strežnika pri shranjevanju dostopnega žetona v bazo podatkov" + }, + { + "id": "api.oauth.get_access_token.internal.app_error", + "translation": "server_error: Pri dostopanju do baze podatkov je prišlo do notranje napake strežnika" + }, + { + "id": "api.oauth.get_access_token.expired_code.app_error", + "translation": "invalid_grant: Neveljavna ali potekla koda avtorizacije" + }, + { + "id": "api.oauth.get_access_token.disabled.app_error", + "translation": "Skrbnik sistema je izklopil ponudnika storitev OAuth2." + }, + { + "id": "api.oauth.get_access_token.credentials.app_error", + "translation": "invalid_client: Neveljavne poverilnice odjemalca" + }, + { + "id": "api.oauth.get_access_token.bad_grant.app_error", + "translation": "invalid_request: Slab grant_type" + }, + { + "id": "api.oauth.get_access_token.bad_client_secret.app_error", + "translation": "invalid_request: Manjkajoči client_secret" + }, + { + "id": "api.oauth.get_access_token.bad_client_id.app_error", + "translation": "invalid_request: Slab client_id" + }, + { + "id": "api.oauth.authorize_oauth.disabled.app_error", + "translation": "Skrbnik sistema je izklopil ponudnika storitev OAuth2." + }, + { + "id": "api.oauth.allow_oauth.turn_off.app_error", + "translation": "Skrbnik sistema je izklopil ponudnika storitev OAuth2." + }, + { + "id": "api.oauth.allow_oauth.redirect_callback.app_error", + "translation": "invalid_request: Priložena redirect_uri se ni ujemala z registriranim callback_url" + }, + { + "id": "api.license.remove_expired_license.failed.error", + "translation": "Neuspešno pošiljanje e-pošte za deaktiviranje računa" + }, + { + "id": "api.license.client.old_format.app_error", + "translation": "Nov format konfiguracije klienta še ni podprt. Nastavi format=old v poizvedbi." + }, + { + "id": "api.license.add_license.unique_users.app_error", + "translation": "Ta licenca podpira samo uporabnike {{.Users}}, če ima vaš sistem {{.Count}} edinstvenih uporabnikov. Unikatni uporabniki se preštejejo ločeno po e-poštnem naslovu. Celotno število uporabnikov lahko vidite v poročilih spletnega mesta -> Prikaži statistiko." + }, + { + "id": "api.license.add_license.save_active.app_error", + "translation": "ID aktivne licence se ni shranil pravilno." + }, + { + "id": "api.license.add_license.save.app_error", + "translation": "Licenca se ni shranila pravilno." + }, + { + "id": "api.license.add_license.open.app_error", + "translation": "Ni mogoče odpreti licence" + }, + { + "id": "api.license.add_license.no_file.app_error", + "translation": "Ni datoteke v polju 'license' v zahtevi" + }, + { + "id": "api.license.add_license.invalid_count.app_error", + "translation": "Skupnih unikatnih uporabnikov ni mogoče prešteti." + }, + { + "id": "api.license.add_license.invalid.app_error", + "translation": "Neveljavna licenca." + }, + { + "id": "api.license.add_license.expired.app_error", + "translation": "Licenca je potekla ali pa se še ni začela." + }, + { + "id": "api.license.add_license.array.app_error", + "translation": "Prazen seznam v polju 'license' v zahtevi" + }, + { + "id": "api.ldap_groups.license_error", + "translation": "vaša licenca ne podpira skupin ldap" + }, + { + "id": "api.ldap_groups.existing_group_name_error", + "translation": "član skupine že obstaja" + }, + { + "id": "api.ldap_group.not_found", + "translation": "Skupina ldap ni bila najdena" + }, + { + "id": "api.io_error", + "translation": "vhod/izhod napaka" + }, + { + "id": "api.incoming_webhook.invalid_username.app_error", + "translation": "Neveljavno uporabniško ime." + }, + { + "id": "api.incoming_webhook.disabled.app_error", + "translation": "Administrator je onemogočil incoming webhooks." + }, + { + "id": "app.import.validate_team_import_data.name_characters.error", + "translation": "Ime ekipe vsebuje neveljavne znake." + }, + { + "id": "app.import.validate_team_import_data.display_name_missing.error", + "translation": "Manjka zahtevana lastnost ekipe: display_name." + }, + { + "id": "app.import.validate_team_import_data.display_name_length.error", + "translation": "Skupina display_name ni znotraj dovoljenih omejitev dolžine." + }, + { + "id": "app.import.validate_team_import_data.description_length.error", + "translation": "Opis ekipe je predolg." + }, + { + "id": "app.import.validate_scheme_import_data.wrong_roles_for_scope.error", + "translation": "Za shemo s tem obsegom so bile zagotovljene napačne vloge." + }, + { + "id": "app.import.validate_scheme_import_data.unknown_scheme.error", + "translation": "Neznano področje sheme." + }, + { + "id": "app.import.validate_scheme_import_data.null_scope.error", + "translation": "Obseg sheme je obvezen." + }, + { + "id": "app.import.validate_scheme_import_data.name_invalid.error", + "translation": "Neveljavno ime sheme." + }, + { + "id": "app.import.validate_scheme_import_data.display_name_invalid.error", + "translation": "Neveljavno ime za prikaz sheme." + }, + { + "id": "app.import.validate_scheme_import_data.description_invalid.error", + "translation": "Neveljaven opis sheme." + }, + { + "id": "app.import.validate_role_import_data.name_invalid.error", + "translation": "Neveljavno ime vloge." + }, + { + "id": "app.import.validate_role_import_data.invalid_permission.error", + "translation": "Neveljavno dovoljenje za vlogo." + }, + { + "id": "app.import.validate_role_import_data.display_name_invalid.error", + "translation": "Neveljavno ime za prikaz vloge." + }, + { + "id": "app.import.validate_role_import_data.description_invalid.error", + "translation": "Neveljaven opis vloge." + }, + { + "id": "app.import.validate_reply_import_data.user_missing.error", + "translation": "Manjka zahtevana lastnost odgovora: Uporabnik." + }, + { + "id": "app.import.validate_reply_import_data.message_missing.error", + "translation": "Manjka zahtevana lastnost odgovora: Sporočilo." + }, + { + "id": "app.import.validate_reply_import_data.message_length.error", + "translation": "Lastnost objave sporočila je daljša od največje dovoljene dolžine." + }, + { + "id": "app.import.validate_reply_import_data.create_at_zero.error", + "translation": "Lastnost reakcije CreateAt ne sme biti enaka nič." + }, + { + "id": "app.import.validate_reply_import_data.create_at_missing.error", + "translation": "Manjka zahtevana lastnost odgovora: create_at." + }, + { + "id": "app.import.validate_reaction_import_data.user_missing.error", + "translation": "Manjka zahtevana lastnost reakcije: Uporabnik." + }, + { + "id": "app.import.validate_reaction_import_data.emoji_name_missing.error", + "translation": "Manjka zahtevana lastnost reakcije: EmojiName." + }, + { + "id": "app.import.validate_reaction_import_data.emoji_name_length.error", + "translation": "Lastnost reakcije EmojiName je daljša od največje dovoljene dolžine." + }, + { + "id": "app.import.validate_reaction_import_data.create_at_zero.error", + "translation": "Lastnost reakcije CreateAt ne sme biti enaka nič." + }, + { + "id": "app.import.validate_reaction_import_data.create_at_missing.error", + "translation": "Manjka zahtevana lastnost reakcije: create_at." + }, + { + "id": "app.import.validate_post_import_data.user_missing.error", + "translation": "Manjka zahtevana lastnost objave: Uporabnik." + }, + { + "id": "app.import.validate_post_import_data.team_missing.error", + "translation": "Manjka zahtevana lastnost objave: Ekipa." + }, + { + "id": "app.import.validate_post_import_data.message_missing.error", + "translation": "Manjka zahtevana lastnost objave: Message." + }, + { + "id": "app.import.validate_post_import_data.message_length.error", + "translation": "Lastnost objave sporočila je daljša od največje dovoljene dolžine." + }, + { + "id": "app.import.validate_post_import_data.create_at_zero.error", + "translation": "Lastnost objave CreateAt ne sme biti nič." + }, + { + "id": "app.import.validate_post_import_data.create_at_missing.error", + "translation": "Manjka zahtevana lastnost objave: create_at." + }, + { + "id": "app.import.validate_post_import_data.channel_missing.error", + "translation": "Manjkajoče zahtevane lastnosti objave: Kanal." + }, + { + "id": "app.import.validate_emoji_import_data.name_missing.error", + "translation": "Uvoženo polje znaka emotikona manjka ali je prazno." + }, + { + "id": "app.import.validate_emoji_import_data.image_missing.error", + "translation": "Uvoženo polje slik emotikona manjka ali je prazno." + }, + { + "id": "app.import.validate_emoji_import_data.empty.error", + "translation": "Uvoz emoji podatkov je prazen." + }, + { + "id": "app.import.validate_direct_post_import_data.user_missing.error", + "translation": "Manjka zahtevana neposredna lastnost objave: uporabnik" + }, + { + "id": "app.import.validate_direct_post_import_data.unknown_flagger.error", + "translation": "Direktno objavo lahko označijo samo člani kanala, v katerem je. \"{{.Username}}\" ni član." + }, + { + "id": "app.import.validate_direct_post_import_data.message_missing.error", + "translation": "Manjkajoča zahteva za direktno objavo: message" + }, + { + "id": "app.import.validate_direct_post_import_data.message_length.error", + "translation": "Sporočilo je predolgo" + }, + { + "id": "app.import.validate_direct_post_import_data.create_at_zero.error", + "translation": "CreateAt mora biti večji od 0" + }, + { + "id": "app.import.validate_direct_post_import_data.create_at_missing.error", + "translation": "Manjka zahtevana neposredna lastnost objave: create_at" + }, + { + "id": "app.import.validate_direct_post_import_data.channel_members_too_many.error", + "translation": "Seznam članov neposrednega objavnega kanala vsebuje preveč elementov" + }, + { + "id": "app.import.validate_direct_post_import_data.channel_members_too_few.error", + "translation": "Seznam članov neposrednega objavnega kanala vsebuje premalo elementov" + }, + { + "id": "app.import.validate_direct_post_import_data.channel_members_required.error", + "translation": "Manjka zahtevana neposredna lastnost objave: channel_members" + }, + { + "id": "app.import.validate_direct_channel_import_data.unknown_favoriter.error", + "translation": "Samo člani lahko dodajo neposredni kanal med priljubljene. \"{{.Username}}\" ni član." + }, + { + "id": "app.import.validate_direct_channel_import_data.members_too_many.error", + "translation": "Seznam neposrednih članov kanala vsebuje preveč elementov" + }, + { + "id": "app.import.validate_direct_channel_import_data.members_too_few.error", + "translation": "Seznam neposrednih članov kanala vsebuje premalo elementov" + }, + { + "id": "app.import.validate_direct_channel_import_data.members_required.error", + "translation": "Manjka zahtevana lastnost direktnega kanala: members" + }, + { + "id": "app.import.validate_direct_channel_import_data.header_length.error", + "translation": "Glava neposrednega kanala je predolga" + }, + { + "id": "app.import.validate_channel_import_data.type_missing.error", + "translation": "Manjka zahtevana lastnost kanala: type." + }, + { + "id": "app.import.validate_channel_import_data.type_invalid.error", + "translation": "Vrsta kanala je neveljavna." + }, + { + "id": "app.import.validate_channel_import_data.team_missing.error", + "translation": "Manjka zahtevana lastnost kanala: team" + }, + { + "id": "app.import.validate_channel_import_data.scheme_invalid.error", + "translation": "Neveljavno ime sheme za kanal." + }, + { + "id": "app.import.validate_channel_import_data.purpose_length.error", + "translation": "Namen kanala je predolg." + }, + { + "id": "app.import.validate_channel_import_data.name_missing.error", + "translation": "Manjka zahtevana lastnost kanala: name" + }, + { + "id": "app.import.validate_channel_import_data.name_length.error", + "translation": "Ime kanala je predolgo." + }, + { + "id": "app.import.validate_channel_import_data.name_characters.error", + "translation": "Ime kanala vsebuje neveljavne znake." + }, + { + "id": "app.import.validate_channel_import_data.header_length.error", + "translation": "Glava kanala je predolga." + }, + { + "id": "app.import.validate_channel_import_data.display_name_length.error", + "translation": "Kanal display_name ni znotraj dovoljenih omejitev dolžine." + }, + { + "id": "app.import.process_import_data_file_version_line.invalid_version.error", + "translation": "Ni možno prebrati različice datoteke za uvoz podatkov." + }, + { + "id": "app.import.import_user_channels.save_preferences.error", + "translation": "Napaka pri uvozu članstva kanala uporabnika. Nastavitev ni bilo mogoče shraniti." + }, + { + "id": "app.import.import_user.save_preferences.error", + "translation": "Napaka pri uvozu uporabnikovih nastavitev. Nastavitev ni bilo mogoče shraniti." + }, + { + "id": "app.import.import_team.scheme_wrong_scope.error", + "translation": "Ekipa mora biti dodeljena shemi, ki je določena za ekipo." + }, + { + "id": "app.import.import_team.scheme_deleted.error", + "translation": "Ni možno uporabiti izbrisane sheme za nastavitev ekipe." + }, + { + "id": "app.import.import_scheme.scope_change.error", + "translation": "Paketni uvoz ne more spremeniti obsega že obstoječe sheme." + }, + { + "id": "app.import.import_post.user_not_found.error", + "translation": "Napaka pri uvozu objave. Uporabnika z uporabniškim imenom \"{{.Username}}\" ni bilo mogoče najti." + }, + { + "id": "app.import.import_post.save_preferences.error", + "translation": "Napaka pri uvozu objave. Nastavitev ni bilo mogoče shraniti." + }, + { + "id": "app.import.import_post.channel_not_found.error", + "translation": "Napaka pri uvozu objave. Kanala z imenom \"{{.ChannelName}}\" ni bilo mogoče najti." + }, + { + "id": "app.import.import_line.unknown_line_type.error", + "translation": "Vrstica za uvoz podatkov ima neznano vrsto \"{{.Type}}\"." + }, + { + "id": "app.import.import_line.null_user.error", + "translation": "Vrstica za uvoz podatkov ima tip \"uporabnik\", vendar ima uporabniški objekt vrednost null." + }, + { + "id": "app.import.import_line.null_team.error", + "translation": "Vrstica za podatke o uvozu ima vrsto »ekipa«, vendar ima predmet ekipe vrednost null." + }, + { + "id": "app.import.import_line.null_scheme.error", + "translation": "Vrstica za uvoz podatkov ima tip \"shema\", vendar ima objekt sheme vrednost null." + }, + { + "id": "api.file.upload_file.too_large_detailed.app_error", + "translation": "Datoteke {{.filename}} ni mogoče naložiti. Dolžina {{.Length}} bajtov presega največje dovoljeno število {{.Limit}} bajtov." + }, + { + "id": "api.file.upload_file.storage.app_error", + "translation": "Slike ni mogoče naložiti. Shramba slik ni nastavljena." + }, + { + "id": "api.file.upload_file.read_request.app_error", + "translation": "Datoteke ni mogoče naložiti. Napaka pri branju ali razčlenjevanju podatkov zahtevka." + }, + { + "id": "api.file.upload_file.read_form_value.app_error", + "translation": "Datoteke ni mogoče naložiti. Napaka pri branju vrednosti za {{.Formname}}." + }, + { + "id": "api.file.upload_file.multiple_channel_ids.app_error", + "translation": "Datotek/e ni mogoče naložiti. Več navzkrižnih ID-jev kanalov." + }, + { + "id": "api.file.upload_file.large_image_detailed.app_error", + "translation": "Dimenzije {{.Filename}} ({{.Width}} do {{.Height}} pikslov) presegajo omejitve" + }, + { + "id": "api.file.upload_file.large_image.app_error", + "translation": "Slika dimenzijsko prevelika: {{.Filename}}" + }, + { + "id": "api.file.upload_file.incorrect_number_of_client_ids.app_error", + "translation": "Datotek/e ni mogoče naložiti. Morate imeti {{.NumClientIds}} client_id za {{.NumFiles}} datotek." + }, + { + "id": "api.file.read_file.reading_local.app_error", + "translation": "Napaka med branjem lokalne strežniške shrambe" + }, + { + "id": "api.file.no_driver.app_error", + "translation": "Ni izbran gonilnik datotek." + }, + { + "id": "api.file.get_public_link.no_post.app_error", + "translation": "Ni mogoče dobiti javne povezave za datoteko. Datoteka mora biti dodana k objavi, ki jo trenutno uporabnik lahko bere." + }, + { + "id": "api.file.get_public_link.disabled.app_error", + "translation": "Javne povezave so onemogočene" + }, + { + "id": "api.file.get_file_thumbnail.no_thumbnail.app_error", + "translation": "Datoteke nima sličice" + }, + { + "id": "api.file.get_file_preview.no_preview.app_error", + "translation": "Datoteka nima predogleda" + }, + { + "id": "api.file.get_file.public_invalid.app_error", + "translation": "Javna povezave ne izgleda veljavna" + }, + { + "id": "api.file.attachments.disabled.app_error", + "translation": "Priponke so onemogočene na strežniku." + }, + { + "id": "api.emoji.upload.open.app_error", + "translation": "Čustvenega simbola ni mogoče ustvariti. Pri poskusu odpiranja priložene slike je prišlo do napake." + }, + { + "id": "api.emoji.upload.large_image.too_large.app_error", + "translation": "Čustvenih simbolov ni mogoče ustvariti. Slika mora biti manjša od {{.MaxWidth}} in {{.MaxHeight}}." + }, + { + "id": "api.emoji.upload.large_image.gif_encode_error", + "translation": "Seška ni mogoče narediti. Kodiranje datoteke GIF ni bilo mogoče." + }, + { + "id": "api.emoji.upload.large_image.gif_decode_error", + "translation": "Smeška ni mogoče narediti. Branje GIF datoteke ni bilo mogoče." + }, + { + "id": "api.emoji.upload.large_image.encode_error", + "translation": "Smeška ni mogoče narediti. Kodiranje slike za emoji ni bilo mogoče." + }, + { + "id": "api.emoji.upload.large_image.decode_error", + "translation": "Smeška ni mogoče narediti. Dekodiranje slike ni bilo uspešno." + }, + { + "id": "api.emoji.upload.image.app_error", + "translation": "Format datoteke je lahko le PNG, JPEG ali GIF." + }, + { + "id": "api.emoji.storage.app_error", + "translation": "Shramba ni pravilno nastavljena. Nastavi lokalno ali S3 shrambo." + }, + { + "id": "api.emoji.get_image.read.app_error", + "translation": "Ne morem prebrati slike za emoji." + }, + { + "id": "api.emoji.get_image.decode.app_error", + "translation": "Ne morem prebrati slike za emoji." + }, + { + "id": "api.emoji.disabled.app_error", + "translation": "Lastne emojije je izklopil administrator." + }, + { + "id": "api.emoji.create.too_large.app_error", + "translation": "Slika za emoji prevelika. Max 1 MB." + }, + { + "id": "api.emoji.create.parse.app_error", + "translation": "Ni mogoče narediti emojia. Napačna zahteva." + }, + { + "id": "api.emoji.create.other_user.app_error", + "translation": "Neveljaven ID uporabnika." + }, + { + "id": "api.emoji.create.duplicate.app_error", + "translation": "Emoji z istim imenom že obstaja." + }, + { + "id": "api.email_batching.send_batched_email_notification.subject", + "translation": { + "few": "[{{.SiteName}}] Nova obvestila za {{.Month}} {{.Day}}, {{.Year}}", + "one": "[{{.SiteName}}] Novo obvezilo za {{.Month}} {{.Day}}, {{.Year}}", + "other": "", + "two": "[{{.SiteName}}] Novi obvestili za {{.Month}} {{.Day}}, {{.Year}}" + } + }, + { + "id": "api.email_batching.add_notification_email_to_batch.disabled.app_error", + "translation": "Povzetek v emailu je bil izklopljen s strani administratorja" + }, + { + "id": "api.email_batching.add_notification_email_to_batch.channel_full.app_error", + "translation": "Predpomnilnik za prejamanje povzetkov aktivnosti je poln. Povečajte EmailBatchingBufferSize." + }, + { + "id": "api.create_terms_of_service.empty_text.app_error", + "translation": "Vnesite besedilo za Pogoji storitve po meri." + }, + { + "id": "api.create_terms_of_service.custom_terms_of_service_disabled.app_error", + "translation": "Funkcija Pogoji storitve po meri je onemogočena" + }, + { + "id": "api.context.token_provided.app_error", + "translation": "Seja ni OAuth ampak žeton je bil podan v spletni poizvedbi" + }, + { + "id": "api.context.session_expired.app_error", + "translation": "Neveljavna ali potekla seja, prijavite se znova." + }, + { + "id": "api.context.permissions.app_error", + "translation": "Nimate ustreznih dovoljenj" + }, + { + "id": "api.context.mfa_required.app_error", + "translation": "Preverjanje v dveh korakih je obvezno na tem strežniku." + }, + { + "id": "api.context.invalid_url_param.app_error", + "translation": "Nepravilen ali napačen parameter {{.Name}} v URLju zahtevka" + }, + { + "id": "api.context.invalid_token.error", + "translation": "Neveljaven token seje {{.Token}}, err={{.Error}}" + }, + { + "id": "api.context.invalid_param.app_error", + "translation": "Napačen parameter {{.Name}}" + }, + { + "id": "api.context.invalid_body_param.app_error", + "translation": "Napačno ali manjkajoče polje {{.Name}} v body zahtevka" + }, + { + "id": "api.context.404.app_error", + "translation": "Strani ni mogoče najti." + }, + { + "id": "api.config.client.old_format.app_error", + "translation": "Nov format konfiguracije klienta še ni podprt. Nastavi format=old v poizvedbi." + }, + { + "id": "api.command_shrug.name", + "translation": "skomig" + }, + { + "id": "api.command_shrug.hint", + "translation": "[sporočilo]" + }, + { + "id": "api.command_shrug.desc", + "translation": "Doda ¯\\_(ツ)_/¯ sporočilu" + }, + { + "id": "api.command_shortcuts.unsupported.app_error", + "translation": "Naprava ne podpira ukaza za bližnjice" + }, + { + "id": "api.command_shortcuts.name", + "translation": "bližnjice" + }, + { + "id": "api.command_shortcuts.desc", + "translation": "Prikaži seznam bližnjic" + }, + { + "id": "api.command_settings.unsupported.app_error", + "translation": "Naprava ne podpira settings ukaza" + }, + { + "id": "api.command_settings.name", + "translation": "nastavitve" + }, + { + "id": "api.command_settings.desc", + "translation": "Odpri nastavitve računa" + }, + { + "id": "api.command_search.unsupported.app_error", + "translation": "Naprava ne podpira ukaza za iskanje" + }, + { + "id": "api.command_search.name", + "translation": "išči" + }, + { + "id": "api.command_search.hint", + "translation": "[tekst]" + }, + { + "id": "api.command_search.desc", + "translation": "Išči med sporočili" + }, + { + "id": "api.command_remove.user_not_in_channel", + "translation": "{{.Username}} ni član tega kanala." + }, + { + "id": "api.command_remove.permission.app_error", + "translation": "Nimate ustreznih pravic za odstranitev člana." + }, + { + "id": "api.command_remove.name", + "translation": "odstrani" + }, + { + "id": "api.command_remove.missing.app_error", + "translation": "Uporabnika ni možno najti. Uporabnika je lahko onemogočil skrbnik sistema." + }, + { + "id": "api.command_remove.message.app_error", + "translation": "Sporočilo mora biti podano z /remove ali /kick ukazom." + }, + { + "id": "api.command_remove.hint", + "translation": "@[username]" + }, + { + "id": "api.command_remove.direct_group.app_error", + "translation": "Osebe ne morete odstraniti iz neposrednega kanala za sporočila." + }, + { + "id": "api.command_remove.desc", + "translation": "Odstrani člana iz kanala" + }, + { + "id": "api.command_open.name", + "translation": "odpri" + }, + { + "id": "api.command_online.success", + "translation": "Zdaj si dosegljiv" + }, + { + "id": "api.command_online.name", + "translation": "dosegljiv" + }, + { + "id": "api.command_online.desc", + "translation": "Nastavi vaše stanje na dosegljiv" + }, + { + "id": "api.command_offline.success", + "translation": "Sedaj ste nedosegljivi" + }, + { + "id": "api.command_offline.name", + "translation": "nedosegljiv" + }, + { + "id": "api.command_offline.desc", + "translation": "Nastavi stanje na nedosegljiv" + }, + { + "id": "api.command_mute.success_unmute_direct_msg", + "translation": "Kanal ni več utišan." + }, + { + "id": "api.command_mute.success_unmute", + "translation": "{{.Channel}} ni več utišan." + }, + { + "id": "api.command_mute.success_mute_direct_msg", + "translation": "Obvestil kanala {{.Channel}} ne boste prejemali dokler ne izključite utišanje kanala." + }, + { + "id": "api.command_mute.success_mute", + "translation": "Obvestil kanala {{.Channel}} ne boste prejemali dokler ne izključite utišanje kanala." + }, + { + "id": "api.command_mute.not_member.error", + "translation": "Kanala {{.Channel}} ni mogoče utišati ker niste član kanala." + }, + { + "id": "api.command_mute.no_channel.error", + "translation": "Podanega kanala ni bilo mogoče najti. Prosim uporabite [channel handle](https://about.mattermost.com/default-channel-handle-documentation) za identifikacijo kanalov." + }, + { + "id": "api.command_mute.name", + "translation": "utišaj" + }, + { + "id": "api.command_mute.hint", + "translation": "~[kanal]" + }, + { + "id": "api.command_mute.error", + "translation": "{{.Channel}} ni možno najti. Prosim uporabite [channel handle](https://about.mattermost.com/default-channel-handle-documentation) za identifikacijo kanalov." + }, + { + "id": "api.command_mute.desc", + "translation": "Izklopi obvesčanje za namizni računalnik, elektronsko sporočilo in potisna obvestila za trenutni kanal ali določen [channel]." + }, + { + "id": "api.command_msg.permission.app_error", + "translation": "Nimate ustreznih pravic za neposredno sporočilo temu uporabniku." + }, + { + "id": "api.command_msg.name", + "translation": "sporočilo" + }, + { + "id": "api.command_msg.missing.app_error", + "translation": "Uporabnika ni mogoče najti." + }, + { + "id": "api.command_msg.hint", + "translation": "@[uporabnik] 'sporočilo'" + }, + { + "id": "api.command_msg.fail.app_error", + "translation": "Napaka med komunikacijo z uporabnikom." + }, + { + "id": "api.command_msg.dm_fail.app_error", + "translation": "Napaka med ustvarjanjem direktnega sporočila." + }, + { + "id": "api.command_msg.desc", + "translation": "Pošlji neposredno sporočilo uporabniku" + }, + { + "id": "api.command_me.name", + "translation": "jaz" + }, + { + "id": "api.command_me.hint", + "translation": "[sporočilo]" + }, + { + "id": "api.command_me.desc", + "translation": "Izvedi dejanje" + }, + { + "id": "api.command_logout.name", + "translation": "odjavi" + }, + { + "id": "api.command_logout.desc", + "translation": "Odjava iz Mattermost-a" + }, + { + "id": "api.command_leave.name", + "translation": "zapusti" + }, + { + "id": "api.command_leave.fail.app_error", + "translation": "Napaka med zapuščanjem kanala." + }, + { + "id": "api.command_leave.desc", + "translation": "Zapusti trenutni kanal" + }, + { + "id": "api.command_kick.name", + "translation": "kick" + }, + { + "id": "api.command_join.name", + "translation": "vstopi" + }, + { + "id": "api.command_join.missing.app_error", + "translation": "Kanala ni bilo mogoče najti" + }, + { + "id": "api.command_join.list.app_error", + "translation": "Napaka med prikazom kanalov." + }, + { + "id": "api.command_join.hint", + "translation": "~[kanal]" + }, + { + "id": "api.command_join.fail.app_error", + "translation": "Prišlo je do napake med vstopom v kanal." + }, + { + "id": "api.command_join.desc", + "translation": "Pridruži se odprtemu kanalu" + }, + { + "id": "api.command_invite_people.permission.app_error", + "translation": "Nimate zadostnih pravic za povabilo novih uporabnikov na ta strežnik." + }, + { + "id": "api.command_invite.user_not_in_team.app_error", + "translation": "{{.Username}} ni član tega kanala." + }, + { + "id": "api.command_invite.user_already_in_channel.app_error", + "translation": "{{.User}} je že v kanalu." + }, + { + "id": "api.command_invite.success", + "translation": "{{.User}} je dodan v {{.Channel}} kanal." + }, + { + "id": "api.command_invite.private_channel.app_error", + "translation": "{{.Channel}} ni možno najti. Prosim uporabite naziv kanala za identifikacijo kanalov." + }, + { + "id": "api.command_invite.permission.app_error", + "translation": "Nimate zadostnih pravic dodati {{.User}} v {{.Channel}}." + }, + { + "id": "api.command_invite.name", + "translation": "povabilo" + }, + { + "id": "api.command_invite.missing_user.app_error", + "translation": "Uporabnika ni možno najti. Skrbnik sistema je lahko uporabnika onemogočil. " + }, + { + "id": "api.command_invite.missing_message.app_error", + "translation": "Manjka uporabniško ime in kanal." + }, + { + "id": "api.command_invite.hint", + "translation": "@[uporabnisko_ime] ~[kanal]" + }, + { + "id": "api.command_invite.fail.app_error", + "translation": "Prišlo je do napake med vstopom v kanal." + }, + { + "id": "api.command_invite.directchannel.app_error", + "translation": "Ni možno dodati osebe v kanal direktnega sporočila." + }, + { + "id": "api.command_invite.desc", + "translation": "Povabi uporabnika v kanal" + }, + { + "id": "api.command_invite.channel.error", + "translation": "{{.Channel}} ni možno najti. Prosim uporabite [channel handle](https://about.mattermost.com/default-channel-handle-documentation) za identifikacijo kanalov." + }, + { + "id": "api.command_invite.channel.app_error", + "translation": "Napaka pridobivanja trenutnega kanala." + }, + { + "id": "api.command_help.name", + "translation": "pomoč" + }, + { + "id": "api.command_help.desc", + "translation": "Odpri Mattermost pomoč" + }, + { + "id": "api.command_groupmsg.permission.app_error", + "translation": "Nimate potrebnih pravic za izdelavo novega sporočila skupine." + }, + { + "id": "api.command_groupmsg.name", + "translation": "sporočilo" + }, + { + "id": "api.command_groupmsg.min_users.app_error", + "translation": "Sporočila v skupini so omejena na minimalno {{.MinUsers}} uporabnikov." + }, + { + "id": "api.command_groupmsg.max_users.app_error", + "translation": "Sporočila v skupini so omejena na maksimalno {{.MaxUsers}} uporabnikov." + }, + { + "id": "api.command_groupmsg.invalid_user.app_error", + "translation": { + "few": "Uporabnikov ni bilo mogoče najti: {{.Users}}", + "one": "Uporabnika ni bilo mogoče najti: {{.Users}}", + "other": "", + "two": "Uporabnika ni bilo mogoče najti: {{.Users}}" + } + }, + { + "id": "api.command_groupmsg.hint", + "translation": "@[username1],@[username2] 'sporočilo'" + }, + { + "id": "api.command_groupmsg.group_fail.app_error", + "translation": "Napaka med ustvarjanjem skupinskega sporočila." + }, + { + "id": "api.command_groupmsg.fail.app_error", + "translation": "Napaka med komunikacijo z uporabnikom." + }, + { + "id": "api.command_groupmsg.desc", + "translation": "Pošlje skupinsko sporočilo določenim uporabnikom" + }, + { + "id": "api.command_expand_collapse.fail.app_error", + "translation": "Prišlo je do napake pri razširjanju predogledov" + }, + { + "id": "api.command_expand.success", + "translation": "Slikovne povezave so prikazane po privzetem" + }, + { + "id": "api.command_expand.name", + "translation": "razširi" + }, + { + "id": "api.command_expand.desc", + "translation": "Izklopi samodejno zlaganje slikovnih povezav" + }, + { + "id": "api.command_echo.name", + "translation": "echo" + }, + { + "id": "api.command_echo.message.app_error", + "translation": "Sporočilo mora biti podano z /echo ukazom." + }, + { + "id": "api.command_echo.hint", + "translation": "'sporočilo' [zakasnitev v sekundah]" + }, + { + "id": "api.command_echo.high_volume.app_error", + "translation": "Velika količina echo sporočil, ni mogoče obdelati zahteve" + }, + { + "id": "api.command_echo.desc", + "translation": "Pošlji sporočilo iz tvojega računa z zakasnitvijo" + }, + { + "id": "api.command_echo.delay.app_error", + "translation": "Zakasnitev mora biti manjša kot 10000 sekund" + }, + { + "id": "api.command_dnd.success", + "translation": "Ne moti je omogočen. Dokler je omogočen ne boste prejemali potisnih obvestil." + }, + { + "id": "api.command_dnd.name", + "translation": "dnd" + }, + { + "id": "api.command_dnd.desc", + "translation": "Ne moti izklopi vsa potisna obvestila." + }, + { + "id": "api.command_collapse.success", + "translation": "Slike so zložene po privzetem" + }, + { + "id": "api.command_collapse.name", + "translation": "zloži" + }, + { + "id": "api.command_collapse.desc", + "translation": "Vklopi samodejno zlaganje predogleda slik" + }, + { + "id": "api.command_code.name", + "translation": "koda" + }, + { + "id": "api.command_code.message.app_error", + "translation": "Sporočilo mora biti podano z /code ukazom." + }, + { + "id": "api.command_code.hint", + "translation": "[tekst]" + }, + { + "id": "api.command_code.desc", + "translation": "Prikaži tekst kot kodo" + }, + { + "id": "api.command_channel_rename.update_channel.app_error", + "translation": "Napaka pri posodabljanju imena kanala." + }, + { + "id": "api.command_channel_rename.too_short.app_error", + "translation": "Ime kanala mora biti dolgo {{.Length}} ali več znakov" + }, + { + "id": "api.command_channel_rename.too_long.app_error", + "translation": "Ime kanala mora biti dolgo {{.Length}} ali manj znakov" + }, + { + "id": "api.command_channel_rename.permission.app_error", + "translation": "Nimate ustreznih pravic za preimenovanje kanala." + }, + { + "id": "api.command_channel_rename.name", + "translation": "preimenuj" + }, + { + "id": "api.command_channel_rename.message.app_error", + "translation": "Sporočilo mora biti podano z /rename ukazom." + }, + { + "id": "api.command_channel_rename.hint", + "translation": "[text]" + }, + { + "id": "api.command_channel_rename.direct_group.app_error", + "translation": "Kanala za neposredno sporočanje ni mogoče urejati." + }, + { + "id": "api.command_channel_rename.desc", + "translation": "Preimenuj kanal" + }, + { + "id": "api.command_channel_rename.channel.app_error", + "translation": "Napaka pridobivanja trenutnega kanala." + }, + { + "id": "api.command_channel_remove.channel.app_error", + "translation": "Napaka pridobivanja trenutnega kanala." + }, + { + "id": "api.command_channel_purpose.update_channel.app_error", + "translation": "Napaka pri posodabljanju kanala." + }, + { + "id": "api.command_channel_purpose.permission.app_error", + "translation": "Nimate ustreznih pravic za urejanje namena kanala." + }, + { + "id": "api.command_channel_purpose.name", + "translation": "namen" + }, + { + "id": "api.command_channel_purpose.message.app_error", + "translation": "Sporočilo mora biti podano s /purpose ukazom." + }, + { + "id": "api.command_channel_purpose.hint", + "translation": "[text]" + }, + { + "id": "api.command_channel_purpose.direct_group.app_error", + "translation": "Ni mogoče nastaviti namena za neposredne kanale. Uporabi /header za nastavitev teme." + }, + { + "id": "api.command_channel_purpose.desc", + "translation": "Uredi namen kanala" + }, + { + "id": "api.command_channel_purpose.channel.app_error", + "translation": "Napaka pridobivanja trenutnega kanala." + }, + { + "id": "api.command_channel_header.update_channel.app_error", + "translation": "Napaka pri posodabljanju trenutnega kanala." + }, + { + "id": "api.command_channel_header.permission.app_error", + "translation": "Nimate ustreznih pravic za urejanje teme kanala." + }, + { + "id": "api.command_channel_header.name", + "translation": "sporočilo kanala" + }, + { + "id": "api.command_channel_header.message.app_error", + "translation": "Tekst mora biti podan z /header ukazom." + }, + { + "id": "api.command_channel_header.hint", + "translation": "[text]" + }, + { + "id": "api.command_channel_header.desc", + "translation": "Uredi sporočilo kanala" + }, + { + "id": "api.command_channel_header.channel.app_error", + "translation": "Napaka pridobivanja trenutnega kanala." + }, + { + "id": "api.command_away.success", + "translation": "Trenutno ste odsotni" + }, + { + "id": "api.command_away.name", + "translation": "odsoten" + }, + { + "id": "api.command_away.desc", + "translation": "Nastavi stanje na odsoten" + }, + { + "id": "api.command.team_mismatch.app_error", + "translation": "Ni mogoče posodobiti ukaze na več ekipah" + }, + { + "id": "api.command.invite_people.sent", + "translation": "Vabila poslana" + }, + { + "id": "api.command.invite_people.no_email", + "translation": "Napiši enega ali več veljavnih email naslovov" + }, + { + "id": "api.command.invite_people.name", + "translation": "povabi_ljudi" + }, + { + "id": "api.command.invite_people.invite_off", + "translation": "Ustvarjanje novih uporabnikov je izklopljeno, vabila niso poslana" + }, + { + "id": "api.command.invite_people.hint", + "translation": "[ime@domena.si ...]" + }, + { + "id": "api.command.invite_people.fail", + "translation": "Napaka pri pošiljanju vabil" + }, + { + "id": "api.command.invite_people.email_off", + "translation": "Email ni nastavljen, vabila niso bila poslana" + }, + { + "id": "api.command.invite_people.email_invitations_off", + "translation": "Elektronska vabila so onemogočena, vabilo ni poslano" + }, + { + "id": "api.command.invite_people.desc", + "translation": "Pošlji vabilo tvoji Mattermost ekipi" + }, + { + "id": "api.command.execute_command.start.app_error", + "translation": "Ukazna akcija ni bila najdena" + }, + { + "id": "api.command.execute_command.not_found.app_error", + "translation": "Ukaz z akcijo '{{.Trigger}}' ni najden. Sporočilom, ki se začnejo z \"/\", dodajte presledek na začetek." + }, + { + "id": "api.command.execute_command.failed_resp.app_error", + "translation": "Ukaz z akcijo '{{.Trigger}}' je vrnil {{.Status}}" + }, + { + "id": "api.team.import_team.no_file.app_error", + "translation": "Ni datoteke v polju 'file' v zahtevi" + }, + { + "id": "api.team.import_team.integer.app_error", + "translation": "Velikost datoteke ni tipa integer" + }, + { + "id": "api.team.import_team.array.app_error", + "translation": "Prazen seznam v polju 'file' v zahtevi" + }, + { + "id": "api.team.get_team_icon.read_file.app_error", + "translation": "Datoteke z ikono ekipe ni mogoče prebrati." + }, + { + "id": "api.team.get_team_icon.filesettings_no_driver.app_error", + "translation": "Neveljavno ime gonilnika za nastavitve datoteke. Mora biti »lokalno« ali »amazons3«" + }, + { + "id": "api.team.get_invite_info.not_open_team", + "translation": "Povabilo je neveljavno, ker to ni odprta ekipa." + }, + { + "id": "api.team.get_all_teams.insufficient_permissions", + "translation": "Nimate ustreznih pravic za urejanje teme kanala." + }, + { + "id": "api.team.demote_user_to_guest.license.error", + "translation": "vaša licenca ne podpira skupin ldap" + }, + { + "id": "api.team.add_user_to_team.missing_parameter.app_error", + "translation": "Za dodajanje uporabnika v skupino je potreben parameter." + }, + { + "id": "api.team.add_user_to_team.added", + "translation": "%v dodan v ekipo - %v" + }, + { + "id": "api.status.user_not_found.app_error", + "translation": "Uporabnika ni možno najti" + }, + { + "id": "api.slackimport.slack_import.zip.app_error", + "translation": "Ni možno odpreti Slack izvozne zip datoteke.\r\n" + }, + { + "id": "api.slackimport.slack_import.team_fail", + "translation": "Ekipe ni možno uvoziti.\r\n" + }, + { + "id": "api.slackimport.slack_import.open.app_error", + "translation": "Ni možno odpreti datoteke: {{.Filename}}.\r\n" + }, + { + "id": "api.command.execute_command.failed_empty.app_error", + "translation": "Ukaz z akcijo '{{.Trigger}}' je vrnil prazen odgovor" + }, + { + "id": "api.command.execute_command.failed.app_error", + "translation": "Ukaz z akcijo '{{.Trigger}}' neuspešen" + }, + { + "id": "api.command.execute_command.create_post_failed.app_error", + "translation": "Ukaz '{{.Trigger}}' ni uspel odgovoriti. Obrnite se na skrbnika sistema." + }, + { + "id": "api.command.duplicate_trigger.app_error", + "translation": "Akcijska beseda je že v uporabi. Izberite novo besedo." + }, + { + "id": "api.command.disabled.app_error", + "translation": "Administrator je onemogočil ukaze." + }, + { + "id": "api.command.command_post.forbidden.app_error", + "translation": "Uporabnik ni član kanala." + }, + { + "id": "api.command.admin_only.app_error", + "translation": "Integracije so bile omejene le na administratorje." + }, + { + "id": "api.channel.update_team_member_roles.scheme_role.app_error", + "translation": "Podana vloga je uporavljana s strani sheme privilegijev in zato ne more biti neposredno dodeljeno članu ekipe." + }, + { + "id": "api.channel.update_channel_scheme.scheme_scope.error", + "translation": "Ni možno nastaviti shemo na kanalu, ker dobavljena shema ni shema kanala." + }, + { + "id": "api.channel.update_channel_scheme.license.error", + "translation": "Vaša licenca ne podpira nadgradnjo sheme kanala" + }, + { + "id": "api.channel.update_channel_member_roles.scheme_role.app_error", + "translation": "Podana vloga je upravljana s strani sheme privilegijev in zato ne more biti neposredno dodeljeno članu kanala" + }, + { + "id": "api.channel.update_channel.tried.app_error", + "translation": "Neveljaven poskus posodobitve privzetega kanala {{.Channel}}" + }, + { + "id": "api.channel.update_channel.deleted.app_error", + "translation": "Kanal je bil arhiviran ali izbrisan" + }, + { + "id": "api.channel.rename_channel.cant_rename_group_messages.app_error", + "translation": "Ne morete preimenovati skupinskega sporočilnega kanala" + }, + { + "id": "api.channel.rename_channel.cant_rename_direct_messages.app_error", + "translation": "Neposrednega sporočilnega kanala ni mogoče preimenovati" + }, + { + "id": "api.channel.remove_user_from_channel.app_error", + "translation": "Uporabnika ni mogoče dodati v kanal te vrste" + }, + { + "id": "api.channel.remove_member.removed", + "translation": "%v je bil odstranjen iz kanala." + }, + { + "id": "api.channel.remove_channel_member.type.app_error", + "translation": "Uporabnika ni možno odstraniti iz kanala" + }, + { + "id": "api.channel.remove.default.app_error", + "translation": "Ni mogoče odstraniti uporabnika iz privzetega kanala {{.Channel}}" + }, + { + "id": "api.channel.post_user_add_remove_message_and_forget.error", + "translation": "Napaka pri objavi prihod/odhod sporočila" + }, + { + "id": "api.channel.post_update_channel_header_message_and_forget.updated_to", + "translation": "%s je posodobil sporočilo kanala v: %s" + }, + { + "id": "api.channel.post_update_channel_header_message_and_forget.updated_from", + "translation": "%s je posodobil sporočilo kanala iz: %s v: %s" + }, + { + "id": "api.channel.post_update_channel_header_message_and_forget.retrieve_user.error", + "translation": "Napaka pridobivanja uporabnika med posodabljanjem sporočila kanala" + }, + { + "id": "api.channel.post_update_channel_header_message_and_forget.removed", + "translation": "%s je odstranil sporočila kanala (prej: %s)" + }, + { + "id": "api.channel.post_update_channel_header_message_and_forget.post.error", + "translation": "Napaka pri sporočanju o spremembi glavnega sporočila" + }, + { + "id": "api.channel.post_update_channel_displayname_message_and_forget.updated_from", + "translation": "%s je posodobil ime kanala iz: %s v: %s" + }, + { + "id": "api.channel.post_update_channel_displayname_message_and_forget.retrieve_user.error", + "translation": "Napaka pridobivanja uporabnika med posodabljanjem imena kanala" + }, + { + "id": "api.channel.post_update_channel_displayname_message_and_forget.create_post.error", + "translation": "Napaka pri objavljanju sporočila o spremembi imena kanala" + }, + { + "id": "api.channel.post_channel_privacy_message.error", + "translation": "Napaka pri objavi sporočila o posodobitvi zasebnosti kanala." + }, + { + "id": "api.channel.patch_update_channel.forbidden.app_error", + "translation": "Posodobitev kanala ni uspela" + }, + { + "id": "api.channel.patch_channel_moderations.license.error", + "translation": "Vaša licenca ne podpira hrambe podatkov." + }, + { + "id": "api.channel.leave.left", + "translation": "%v je zapustil kanal." + }, + { + "id": "api.channel.leave.last_member.app_error", + "translation": "Zadnji si, poskusi odstraniti kanal Privatni kanal namesto zapuščanja." + }, + { + "id": "api.channel.leave.direct.app_error", + "translation": "Ni mogoče zapustiti neposrednega kanala" + }, + { + "id": "api.channel.leave.default.app_error", + "translation": "Ni mogoče zapustiti privzetega kanala {{.Channel}}" + }, + { + "id": "api.channel.join_channel.post_and_forget", + "translation": "%v se je pridružil kanalu." + }, + { + "id": "api.channel.join_channel.permissions.app_error", + "translation": "Nimate ustreznih dovoljenj" + }, + { + "id": "api.channel.get_channel_moderations.license.error", + "translation": "Vaša licenca ne podpira hrambe podatkov." + }, + { + "id": "api.channel.delete_channel.type.invalid", + "translation": "Kanalov skupine ali direktnih sporočil ni mogoče brisati" + }, + { + "id": "api.channel.delete_channel.deleted.app_error", + "translation": "Kanal je bil arhiviran ali izbrisan" + }, + { + "id": "api.channel.delete_channel.cannot.app_error", + "translation": "Privzetega kanala ni mogoče izbrisati {{.Channel}}" + }, + { + "id": "api.channel.delete_channel.archived", + "translation": "%v je arhiviral kanal." + }, + { + "id": "api.channel.create_group.bad_user.app_error", + "translation": "En od navedenih uporabnikov ne obstaja" + }, + { + "id": "api.channel.create_group.bad_size.app_error", + "translation": "Kanali za skupinski pogovor imajo lahko najmanj 3 in največ 8 uporabnikov" + }, + { + "id": "api.channel.create_direct_channel.invalid_user.app_error", + "translation": "Napačen ID uporabnika za ustvarjanje neposrednega kanala" + }, + { + "id": "api.channel.create_default_channels.town_square", + "translation": "Trg" + }, + { + "id": "api.channel.create_default_channels.off_topic", + "translation": "Off-Topic" + }, + { + "id": "api.channel.create_channel.max_channel_limit.app_error", + "translation": "Ni mogoče ustvariti več kot {{.MaxChannelsPerTeam}} kanalov za trenutno ekipo" + }, + { + "id": "api.channel.create_channel.direct_channel.app_error", + "translation": "Uporabiti morate createDirectChannel API servis za ustvarjanje neposrednih kanalov" + }, + { + "id": "api.channel.channel_member_counts_by_group.license.error", + "translation": "vaša licenca ne podpira skupin ldap" + }, + { + "id": "api.channel.change_channel_privacy.public_to_private", + "translation": "Ta kanal je bil spremenjen v zasebni kanal." + }, + { + "id": "api.channel.change_channel_privacy.private_to_public", + "translation": "Ta kanal je bil spremenjen v javni kanal in do njega lahko dostopa vsak član ekipe." + }, + { + "id": "api.channel.add_user_to_channel.type.app_error", + "translation": "Uporabnika ni mogoče dodati v kanal te vrste" + }, + { + "id": "api.channel.add_user.to.channel.failed.deleted.app_error", + "translation": "Napaka pri dodajanju uporabnika v kanal, ker je bil odstranjen iz ekipe." + }, + { + "id": "api.channel.add_user.to.channel.failed.app_error", + "translation": "Napaka pri dodajanju uporabnika v kanal" + }, + { + "id": "api.channel.add_member.added", + "translation": "%v dodan v kanal - %v" + }, + { + "id": "api.channel.add_guest.added", + "translation": "%v dodan v kanal - %v" + }, + { + "id": "api.bot.teams_channels.add_message_mobile", + "translation": "Prosim dodajte me ekipam in kanalom, v katerih želite da sodelujem. Če želite to narediti, uporabite brskalnik ali Mattermost Desktop App." + }, + { + "id": "api.bot.create_disabled", + "translation": "Kreiranje robotka je bilo onemogočeno." + }, + { + "id": "api.admin.upload_brand_image.too_large.app_error", + "translation": "Napaka pri nalaganju. Datoteka je prevelika." + }, + { + "id": "api.admin.upload_brand_image.storage.app_error", + "translation": "Slike ni mogoče naložiti. Shramba slik ni nastavljena." + }, + { + "id": "api.admin.upload_brand_image.parse.app_error", + "translation": "Ni mogoče prebrati multipart obrazca" + }, + { + "id": "api.admin.upload_brand_image.no_file.app_error", + "translation": "Ni datoteke v polju 'slika' v zahtevi" + }, + { + "id": "api.admin.upload_brand_image.array.app_error", + "translation": "Prazen seznam v polju 'image' v zahtevi" + }, + { + "id": "api.admin.test_s3.missing_s3_bucket", + "translation": "S3 Bucket je zahtevan" + }, + { + "id": "api.admin.test_email.subject", + "translation": "Mattermost - Preizkus nastavitve Email" + }, + { + "id": "api.admin.test_email.reenter_password", + "translation": "SMTP nastavitve za strežnik, vrata ali uporabniško ime so se spremenile. Za preizkus znova vnesite SMTP geslo." + }, + { + "id": "api.admin.test_email.missing_server", + "translation": "SMTP strežnik je obvezen" + }, + { + "id": "api.admin.test_email.body", + "translation": "Email nastavitve so pravilne!" + }, + { + "id": "api.admin.saml.not_available.app_error", + "translation": "SAML 2.0 ni podprt ali nastavljen na strežniku." + }, + { + "id": "api.admin.saml.metadata.app_error", + "translation": "Napaka pri izdelavi Service Provider Metadata." + }, + { + "id": "api.admin.saml.failure_save_idp_certificate_file.app_error", + "translation": "Ni mogoče shraniti certifikata." + }, + { + "id": "api.admin.remove_certificate.delete.app_error", + "translation": "Pri brisanju certifikata je prišlo do napake." + }, + { + "id": "api.admin.get_brand_image.storage.app_error", + "translation": "Shramba slik ni nastavljena." + }, + { + "id": "api.admin.file_read_error", + "translation": "Napaka pri branju dnevnika." + }, + { + "id": "api.admin.delete_brand_image.storage.not_found", + "translation": "Ni možno izbrisati slike znamke. Slika ni bila najdena." + }, + { + "id": "api.admin.add_certificate.saving.app_error", + "translation": "Ni mogoče shraniti certifikata." + }, + { + "id": "api.admin.add_certificate.open.app_error", + "translation": "Ni mogoče odpreti datoteke s certifikatom." + }, + { + "id": "api.admin.add_certificate.no_file.app_error", + "translation": "Ni datoteke v 'certificate' v zahtevi." + }, + { + "id": "api.admin.add_certificate.array.app_error", + "translation": "V zahtevi ni datoteke 'certificate'." + }, + { + "id": "September", + "translation": "september" + }, + { + "id": "October", + "translation": "oktober" + }, + { + "id": "November", + "translation": "november" + }, + { + "id": "May", + "translation": "maj" + }, + { + "id": "March", + "translation": "marec" + }, + { + "id": "June", + "translation": "junij" + }, + { + "id": "July", + "translation": "julij" + }, + { + "id": "January", + "translation": "januar" + }, + { + "id": "February", + "translation": "februar" + }, + { + "id": "December", + "translation": "december" + }, + { + "id": "August", + "translation": "avgust" + }, + { + "id": "April", + "translation": "april" + }, + { + "id": "model.group.name.invalid_length.app_error", + "translation": "Ime mora vsebovati od 1 do 64 malih alfanumeričnih znakov" + }, + { + "id": "api.admin.ldap.not_available.app_error", + "translation": "LDAP ni na voljo." + } +] diff --git a/server/i18n/sq.json b/server/i18n/sq.json new file mode 100644 index 00000000000..caeae127198 --- /dev/null +++ b/server/i18n/sq.json @@ -0,0 +1,130 @@ +[ + { + "id": "ent.saml.service_disable.app_error", + "translation": "SAML 2.0 nuk është i konfiguruar ose i përkrahur në këtë server." + }, + { + "id": "api.user.upload_profile_user.open.app_error", + "translation": "E pamundur hapja e skedarit të çertifikatës." + }, + { + "id": "api.user.upload_profile_user.no_file.app_error", + "translation": "Asnjë skedar nën 'imazh' të kërkuar" + }, + { + "id": "api.user.saml.not_available.app_error", + "translation": "SAML 2.0 nuk është i konfiguruar ose i përkrahur në këtë server." + }, + { + "id": "api.team.set_team_icon.open.app_error", + "translation": "E pamundur hapja e skedarit të çertifikatës." + }, + { + "id": "api.team.set_team_icon.no_file.app_error", + "translation": "Asnjë skedar nën 'imazh' të kërkuar" + }, + { + "id": "api.team.import_team.open.app_error", + "translation": "Nuk mund te hapet skedari." + }, + { + "id": "api.team.import_team.no_file.app_error", + "translation": "Asnjë skedar nën 'skedar' të kërkuar" + }, + { + "id": "api.license.add_license.open.app_error", + "translation": "E pamundur hapja e skedarit të çertifikatës." + }, + { + "id": "api.license.add_license.no_file.app_error", + "translation": "Asnjë skedar nën 'çertifikatë' të kërkuar" + }, + { + "id": "api.command.invite_people.name", + "translation": "invite_people" + }, + { + "id": "api.admin.upload_brand_image.no_file.app_error", + "translation": "Asnjë skedar nën 'imazh' të kërkuar" + }, + { + "id": "api.admin.saml.not_available.app_error", + "translation": "SAML 2.0 nuk është i konfiguruar ose i përkrahur në këtë server." + }, + { + "id": "api.admin.saml.failure_save_idp_certificate_file.app_error", + "translation": "E pamundur ruajtja e skedarit të çertifikatës." + }, + { + "id": "api.admin.get_brand_image.storage.app_error", + "translation": "Ruajtja e imazhit nuk është e konfiguruar." + }, + { + "id": "api.admin.file_read_error", + "translation": "Gabim gjatë leximit të skedarit të log." + }, + { + "id": "api.admin.add_certificate.saving.app_error", + "translation": "E pamundur ruajtja e skedarit të çertifikatës." + }, + { + "id": "api.admin.add_certificate.open.app_error", + "translation": "E pamundur hapja e skedarit të çertifikatës." + }, + { + "id": "api.admin.add_certificate.no_file.app_error", + "translation": "Asnjë skedar nën 'çertifikatë' të kërkuar." + }, + { + "id": "api.admin.add_certificate.array.app_error", + "translation": "Asnjë skedar nën 'çertifikatë' të kërkuar." + }, + { + "id": "September", + "translation": "Shtator" + }, + { + "id": "October", + "translation": "Tetor" + }, + { + "id": "November", + "translation": "Nëntor" + }, + { + "id": "May", + "translation": "Maj" + }, + { + "id": "March", + "translation": "Mars" + }, + { + "id": "June", + "translation": "Qershor" + }, + { + "id": "July", + "translation": "Korrik" + }, + { + "id": "January", + "translation": "Janar" + }, + { + "id": "February", + "translation": "Shkurt" + }, + { + "id": "December", + "translation": "Dhjetor" + }, + { + "id": "August", + "translation": "Gusht" + }, + { + "id": "April", + "translation": "Prill" + } +] diff --git a/server/i18n/sr.json b/server/i18n/sr.json new file mode 100644 index 00000000000..38241dc2e07 --- /dev/null +++ b/server/i18n/sr.json @@ -0,0 +1,1126 @@ +[ + { + "id": "web.incoming_webhook.disabled.app_error", + "translation": "Долазне веб куке су онемогућене од стране админа система." + }, + { + "id": "model.token.is_valid.size", + "translation": "Неисправан жетон." + }, + { + "id": "model.team.is_valid.reserved.app_error", + "translation": "Овај URL није доступан. Пробајте други." + }, + { + "id": "ent.saml.service_disable.app_error", + "translation": "SAML 2.0 није подешен или подржан на овом серверу." + }, + { + "id": "ent.saml.metadata.app_error", + "translation": "Догодила се грешка приликом изградње мета-података достављача услуга." + }, + { + "id": "app.import.import_post.user_not_found.error", + "translation": "Грешка при увозу канала. Нисам могао да нађем тим са именом \"{{.TeamName}}\"." + }, + { + "id": "app.import.import_post.save_preferences.error", + "translation": "Грешка при увозу објаве. Нисам успео да сачувам поставке." + }, + { + "id": "app.import.import_post.channel_not_found.error", + "translation": "Грешка при увозу канала. Нисам могао да нађем тим са именом \"{{.TeamName}}\"." + }, + { + "id": "app.import.import_line.null_user.error", + "translation": "Увозни ред са податком садржи врсту \"user\" али је user објекат null." + }, + { + "id": "app.import.import_line.null_team.error", + "translation": "Увозни ред са податком садржи врсту \"team\" али је објекат тима null." + }, + { + "id": "app.import.import_line.null_post.error", + "translation": "Увозни ред са податком садржи врсту \"post\" али је post_object објекат null." + }, + { + "id": "app.import.import_line.null_direct_post.error", + "translation": "Увозни ред са податком има врсту \"direct_post\" али је direct_post објекат null." + }, + { + "id": "app.import.import_line.null_direct_channel.error", + "translation": "Увозни ред са податком има врсту \"direct_channel\" али је direct_channel објекат null." + }, + { + "id": "app.import.import_line.null_channel.error", + "translation": "Увозни ред са податком садржи врсту \"channel\" али је објекат канала null." + }, + { + "id": "app.import.import_direct_post.create_group_channel.error", + "translation": "Нисам могао да добавим групни канал" + }, + { + "id": "app.import.import_direct_post.create_direct_channel.error", + "translation": "Нисам могао да добавим директни канал" + }, + { + "id": "app.import.import_direct_channel.update_header_failed.error", + "translation": "Нисам успео да ажурирам заглавље директног канала" + }, + { + "id": "app.import.import_direct_channel.create_group_channel.error", + "translation": "Нисам успео да направим групни канал" + }, + { + "id": "app.import.import_direct_channel.create_direct_channel.error", + "translation": "Нисам успео да направим директан канал" + }, + { + "id": "app.import.import_channel.team_not_found.error", + "translation": "Грешка при увозу канала. Нисам могао да нађем тим са именом \"{{.TeamName}}\"." + }, + { + "id": "app.import.bulk_import.json_decode.error", + "translation": "Декодирање JSON реда није успело." + }, + { + "id": "app.import.bulk_import.file_scan.error", + "translation": "Грешка приликом читања датотеке са увезеним подацима." + }, + { + "id": "app.channel.post_update_channel_purpose_message.updated_to", + "translation": "Корисник %s је изменио опис сврхе канала у: %s" + }, + { + "id": "app.channel.post_update_channel_purpose_message.updated_from", + "translation": "Корисник %s је изменио опис сврхе канала из: %s у: %s" + }, + { + "id": "app.channel.post_update_channel_purpose_message.retrieve_user.error", + "translation": "Нисам успео да добавим корисника приликом ажурирања поруке намене канала %v" + }, + { + "id": "app.channel.post_update_channel_purpose_message.removed", + "translation": "Корисник %s је уклонио опис сврхе канала (био је: %s)" + }, + { + "id": "app.channel.post_update_channel_purpose_message.post.error", + "translation": "Нисам успео да објавим поруку са сврхом канала" + }, + { + "id": "api.webhook.update_outgoing.intersect.app_error", + "translation": "Одлазне веб куке са истог канала не могу имати истоветне окидачке URL-ове за речи и повратне позиве." + }, + { + "id": "api.webhook.create_outgoing.permissions.app_error", + "translation": "Неисправна овлашћења за стварање одлазне веб куке." + }, + { + "id": "api.webhook.create_outgoing.not_open.app_error", + "translation": "Прављење одлазних веб кука се може радити само за јавне канале." + }, + { + "id": "api.webhook.create_outgoing.intersect.app_error", + "translation": "Одлазне веб куке са истог канала не могу имати истоветне окидачке URL-ове за речи и повратне позиве." + }, + { + "id": "api.user.verify_email.bad_link.app_error", + "translation": "Лоша веза за потврду мејла." + }, + { + "id": "api.user.upload_profile_user.too_large.app_error", + "translation": "Не могу да отпремим профилну слику. Сувише је велика." + }, + { + "id": "api.user.upload_profile_user.storage.app_error", + "translation": "Не могу да отпремим датотеку. Складиште датотека није подешено." + }, + { + "id": "api.user.update_password.menu", + "translation": "корисшћењем менија са подешавањима" + }, + { + "id": "api.user.update_password.incorrect.app_error", + "translation": "Унос у пољу “Тренутна лозинка“ није исправан. Проверите да ли је Caps Lock лампица искључена и пробајте поново." + }, + { + "id": "api.user.send_verify_email_and_forget.failed.error", + "translation": "Нисам успео да успешно пошаљем потврдни мејл" + }, + { + "id": "api.user.send_sign_in_change_email_and_forget.error", + "translation": "Нисам успео да успешно пошаљем мејл за ажурирање лозинке" + }, + { + "id": "api.user.send_email_change_verify_email_and_forget.error", + "translation": "Нисам успео да успешно пошаљем мејл обавештење о потврди мејла" + }, + { + "id": "api.user.saml.not_available.app_error", + "translation": "SAML 2.0 није подешен или подржан на овом серверу." + }, + { + "id": "api.user.reset_password.method", + "translation": "коришћењем везе за обнову лозинке" + }, + { + "id": "api.user.create_user.signup_email_disabled.app_error", + "translation": "Тимска пријава преко мејла је онемогућена." + }, + { + "id": "api.user.check_user_mfa.bad_code.app_error", + "translation": "Неисправан MFA жетон." + }, + { + "id": "api.user.authorize_oauth_user.invalid_state.app_error", + "translation": "Неисправно стање" + }, + { + "id": "api.templates.welcome_body.info", + "translation": "Ова адреса е-поште је коришћена за креирање налога код Маттермост-а." + }, + { + "id": "api.templates.welcome_body.button", + "translation": "Потврдите мејл" + }, + { + "id": "api.templates.verify_body.info", + "translation": "Ова адреса е-поште је коришћена за креирање налога код Маттермост-а." + }, + { + "id": "api.templates.verify_body.button", + "translation": "Потврдите мејл" + }, + { + "id": "api.templates.username_change_subject", + "translation": "[{{ .SiteName }}] Ваше корисничко име је промењено" + }, + { + "id": "api.templates.username_change_body.title", + "translation": "Ажурирали сте ваше корисничко име" + }, + { + "id": "api.templates.post_body.button", + "translation": "Одговорите у Маттермост" + }, + { + "id": "api.templates.invite_body.title", + "translation": "{{ .SenderName }} вас је позвао да се придружите {{ .TeamDisplayName }} тиму." + }, + { + "id": "api.templates.invite_body.button", + "translation": "Придружи се сад" + }, + { + "id": "api.templates.email_organization", + "translation": "Послао корисник " + }, + { + "id": "api.templates.email_footer", + "translation": "Да бисте променили подешавања обавештења, пријавите се на сајт свог тима и идите на Подешавања > Обавештења." + }, + { + "id": "api.templates.email_change_verify_subject", + "translation": "[{{ .SiteName }}] Потврдите нову мејл адресу" + }, + { + "id": "api.templates.email_change_verify_body.title", + "translation": "Успешно сте ажурирали своју е-пошту" + }, + { + "id": "api.templates.email_change_verify_body.info", + "translation": "Да бисте завршили са ажурирањем ваше мејл адресе за тим {{.TeamDisplayName}}, кликните на везу испод да бисте потврдили да је ово исправна адреса." + }, + { + "id": "api.templates.email_change_verify_body.button", + "translation": "Потврдите мејл" + }, + { + "id": "api.templates.email_change_subject", + "translation": "[{{ .SiteName }}] Ваша мејл адреса је промењена" + }, + { + "id": "api.templates.email_change_body.title", + "translation": "Ажурирали сте ваш мејл" + }, + { + "id": "api.team.update_member_roles.not_a_member", + "translation": "Наведени корисник није члан наведеног тима." + }, + { + "id": "api.team.remove_user_from_team.missing.app_error", + "translation": "Изгледа да корисник није део овог тима." + }, + { + "id": "api.team.invite_members.no_one.app_error", + "translation": "Немам кога да позовем." + }, + { + "id": "api.team.invite_members.invalid_email.app_error", + "translation": "Следеће мејл адресе не припадају прихваћеном домену: {{.Addresses}}. Контактирајте вашег систем администратора за више детаља." + }, + { + "id": "api.team.import_team.unavailable.app_error", + "translation": "Изобличен захтев: поље filesize није присутно." + }, + { + "id": "api.team.get_invite_info.not_open_team", + "translation": "Позивница је неважећа јер ово није отворени тим." + }, + { + "id": "api.team.add_user_to_team.missing_parameter.app_error", + "translation": "Потребан је параметар за додавање корисника у тим." + }, + { + "id": "api.slackimport.slack_import.team_fail", + "translation": "Не могу да добавим тим да бих увезао у њега.\r\n" + }, + { + "id": "api.slackimport.slack_import.open.app_error", + "translation": "Не могу да отворим датотеку: {{.Filename}}.\r\n" + }, + { + "id": "api.slackimport.slack_import.notes", + "translation": "\r\nБелешке:\r\n" + }, + { + "id": "api.slackimport.slack_import.note3", + "translation": "- Додатне грешке можете наћи у записницима на серверу.\r\n" + }, + { + "id": "api.slackimport.slack_add_users.email_pwd", + "translation": "Slack корисник са мејлом {{.Email}} и лозинком {{.Password}} је увежен.\r\n" + }, + { + "id": "api.slackimport.slack_add_users.created", + "translation": "\r\nКорисника направљено:\r\n" + }, + { + "id": "api.slackimport.slack_add_channels.merge", + "translation": "Slack канал {{.DisplayName}} већ постоји као активни Mattermost канал. Оба канала су спојена.\r\n" + }, + { + "id": "api.slackimport.slack_add_channels.import_failed", + "translation": "Не могу да увезем Slack канал {{.DisplayName}}.\r\n" + }, + { + "id": "api.slackimport.slack_add_channels.failed_to_add_user", + "translation": "Не могу да додам Slack корисника {{.Username}} на канал.\r\n" + }, + { + "id": "api.slackimport.slack_add_channels.added", + "translation": "\r\nДодати канали:\r\n" + }, + { + "id": "api.slackimport.slack_add_bot_user.unable_import", + "translation": "Не могу да увезем интеграционог Slack бот корисника {{.Username}}.\r\n" + }, + { + "id": "api.slackimport.slack_add_bot_user.email_pwd", + "translation": "Интеграциони Slack бот корисник са мејлом {{.Email}} и лозинком {{.Password}} је увежен.\r\n" + }, + { + "id": "api.server.start_server.starting.critical", + "translation": "Грешка при покретању сервера, err: %v" + }, + { + "id": "api.server.start_server.rate_limiting_rate_limiter", + "translation": "Не могу да иницијализујем ограничавање стопа." + }, + { + "id": "api.server.start_server.rate_limiting_memory_store", + "translation": "Не могу да иницијализујем меморијско складиште за ограничавање стопа. Проверите подешавање MemoryStoreSize." + }, + { + "id": "api.reaction.save_reaction.user_id.app_error", + "translation": "Не можете сачувати реакцију за другог корисника." + }, + { + "id": "api.reaction.save_reaction.invalid.app_error", + "translation": "Реакција није исправна." + }, + { + "id": "api.post.link_preview_disabled.app_error", + "translation": "Претпрегледи веза су онемогућени од стране администратора система." + }, + { + "id": "api.post.get_message_for_notification.images_sent", + "translation": { + "few": "{{.Count}} слике послате: {{.Filenames}}", + "one": "{{.Count}} слика послата: {{.Filenames}}", + "other": "{{.Count}} слика послато: {{.Filenames}}" + } + }, + { + "id": "api.post.get_message_for_notification.files_sent", + "translation": { + "few": "{{.Count}} датотеке је послато: {{.Filenames}}", + "one": "{{.Count}} датотека је послата: {{.Filenames}}", + "other": "{{.Count}} датотека је послато: {{.Filenames}}" + } + }, + { + "id": "api.post.disabled_here", + "translation": "@here је онемогућено зато што канал има више од {{.Users}} корисника." + }, + { + "id": "api.post.disabled_channel", + "translation": "@channel је онемогућено зато што канал има више од {{.Users}} корисника." + }, + { + "id": "api.post.disabled_all", + "translation": "@all је онемогућено зато што канал има више од {{.Users}} корисника." + }, + { + "id": "api.post.create_post.can_not_post_to_deleted.error", + "translation": "Не могу да објављујем унутар обрисаног канала." + }, + { + "id": "api.oauth.singup_with_oauth.disabled.app_error", + "translation": "Регистровање корисника је онемогућено." + }, + { + "id": "api.oauth.register_oauth_app.turn_off.app_error", + "translation": "Админ система је искључио достављача OAuth2 услуге." + }, + { + "id": "api.oauth.get_access_token.disabled.app_error", + "translation": "Админ система је искључио достављача OAuth2 услуге." + }, + { + "id": "api.oauth.authorize_oauth.disabled.app_error", + "translation": "Админ система је искључио достављача OAuth2 услуге." + }, + { + "id": "api.oauth.allow_oauth.turn_off.app_error", + "translation": "Админ система је искључио достављача OAuth2 услуге." + }, + { + "id": "api.license.client.old_format.app_error", + "translation": "Нови формат за дозволу клијента још није подржан. Наведите format=old у реченици упита." + }, + { + "id": "api.license.add_license.unique_users.app_error", + "translation": "Ова лиценца подржава само {{.Users}} корисника а ваш систем има {{.Count}} јединствених корисника. Јединствени корисници се броје на основу мејл адресе. Можете видети укупан број корисника у делу Извештаји сајта > Прикажи статистику." + }, + { + "id": "api.license.add_license.save_active.app_error", + "translation": "ИБ активне лиценце није исправно сачуван." + }, + { + "id": "api.license.add_license.save.app_error", + "translation": "Лиценца није исправно сачувана." + }, + { + "id": "api.license.add_license.invalid_count.app_error", + "translation": "Не могу да пребројим све јединствене кориснике." + }, + { + "id": "api.license.add_license.invalid.app_error", + "translation": "Неисправна датотека са лиценцом." + }, + { + "id": "api.license.add_license.expired.app_error", + "translation": "Лиценца је или истекла или још није започета." + }, + { + "id": "api.file.upload_file.storage.app_error", + "translation": "Не могу да отпремим слику. Складиште слика није подешено." + }, + { + "id": "api.file.upload_file.large_image.app_error", + "translation": "Не може се отпремити датотека која прекорачује највеће димензије: {{.Filename}}" + }, + { + "id": "api.file.get_public_link.no_post.app_error", + "translation": "Не могу да добавим јавну везу за датотеку. Датотека мора бити приложена објави коју може прочитати тренутни корисник." + }, + { + "id": "api.file.attachments.disabled.app_error", + "translation": "Прилози са датотекама су онемогућени на овом серверу." + }, + { + "id": "api.emoji.upload.large_image.gif_encode_error", + "translation": "Не могу да направим емоџи. Догодила се грешка приликом кодирања GIF слике." + }, + { + "id": "api.emoji.upload.large_image.gif_decode_error", + "translation": "Не могу да направим емоџи. Догодила се грешка приликом декодирања GIF слике." + }, + { + "id": "api.emoji.upload.large_image.encode_error", + "translation": "Не могу да направим емоџи. Догодила се грешка приликом кодирања слике." + }, + { + "id": "api.emoji.upload.large_image.decode_error", + "translation": "Не могу да направим емоџи. Догодила се грешка приликом декодирања слике." + }, + { + "id": "api.emoji.upload.image.app_error", + "translation": "Не могу да направим емоџи. Датотека мора бити PNG, JPEG или GIF." + }, + { + "id": "api.emoji.storage.app_error", + "translation": "Складиште слика није исправно подешено. Подесите било S3 или локално серверско складиште датотека." + }, + { + "id": "api.emoji.get_image.read.app_error", + "translation": "Не могу да прочитам слику за емоџи." + }, + { + "id": "api.emoji.get_image.decode.app_error", + "translation": "Не могу да декодирам слику за емоџи." + }, + { + "id": "api.emoji.disabled.app_error", + "translation": "Прилагођени емоџији су онемогућени од стране админа система." + }, + { + "id": "api.emoji.create.too_large.app_error", + "translation": "Нисам успео да направим емоџи. Слике морају бити мање од 1 MB." + }, + { + "id": "api.emoji.create.parse.app_error", + "translation": "Нисам успео да направим емоџи. Не разумем захтев." + }, + { + "id": "api.emoji.create.duplicate.app_error", + "translation": "Нисам успео да направим емоџи. Други емоџи са истим називом већ постоји." + }, + { + "id": "api.email_batching.send_batched_email_notification.subject", + "translation": { + "few": "[{{.SiteName}}] Нова обавештења за {{.Month}} {{.Day}}, {{.Year}}", + "one": "[{{.SiteName}}] Ново обавештење за {{.Month}} {{.Day}}, {{.Year}}", + "other": "[{{.SiteName}}] Нова обавештења за {{.Month}} {{.Day}}, {{.Year}}" + } + }, + { + "id": "api.email_batching.add_notification_email_to_batch.channel_full.app_error", + "translation": "Канал за примање серијских мејл послова је пун. Повећајте EmailBatchingBufferSize." + }, + { + "id": "api.context.session_expired.app_error", + "translation": "Неисправна или истекла сесија, пријавите се поново." + }, + { + "id": "api.context.mfa_required.app_error", + "translation": "Вишефакторска идентификација је обавезна на овом серверу." + }, + { + "id": "api.context.invalid_token.error", + "translation": "Неисправан жетон сесије={{.Token}}, err={{.Error}}" + }, + { + "id": "api.context.404.app_error", + "translation": "Нажалост, нисмо могли да нађемо страницу." + }, + { + "id": "api.config.client.old_format.app_error", + "translation": "Нови формат за подешавање клијента још није подржан. Наведите format=old у реченици упита." + }, + { + "id": "api.command_shrug.name", + "translation": "слегни-раменима" + }, + { + "id": "api.command_shrug.hint", + "translation": "[порука]" + }, + { + "id": "api.command_shrug.desc", + "translation": "Додаје ¯\\_(ツ)_/¯ у вашу поруку" + }, + { + "id": "api.command_shortcuts.name", + "translation": "пречице" + }, + { + "id": "api.command_shortcuts.desc", + "translation": "Приказује списак пречица на тастатури" + }, + { + "id": "api.command_settings.name", + "translation": "подешавања" + }, + { + "id": "api.command_settings.desc", + "translation": "Отворите дијалог Подешавања" + }, + { + "id": "api.command_search.name", + "translation": "тражи" + }, + { + "id": "api.command_search.hint", + "translation": "[текст]" + }, + { + "id": "api.command_search.desc", + "translation": "Тражи текст у порукама" + }, + { + "id": "api.command_remove.message.app_error", + "translation": "Потребно је доставити поруку са наредбом /уклони или /избаци." + }, + { + "id": "api.command_open.name", + "translation": "отвори" + }, + { + "id": "api.command_online.success", + "translation": "Сада сте на мрежи" + }, + { + "id": "api.command_online.name", + "translation": "на мрежи" + }, + { + "id": "api.command_online.desc", + "translation": "Поставите да сте на мрежи" + }, + { + "id": "api.command_offline.success", + "translation": "Сада сте ван мреже" + }, + { + "id": "api.command_offline.name", + "translation": "ван мреже" + }, + { + "id": "api.command_offline.desc", + "translation": "Поставите да сте ван мреже" + }, + { + "id": "api.command_msg.name", + "translation": "порука" + }, + { + "id": "api.command_msg.hint", + "translation": "@[корисничко] 'порука'" + }, + { + "id": "api.command_msg.fail.app_error", + "translation": "Догодила се грешка приликом слања поруке кориснику." + }, + { + "id": "api.command_msg.dm_fail.app_error", + "translation": "Догодила се грешка приликом стварања директне поруке." + }, + { + "id": "api.command_msg.desc", + "translation": "Пошаљи директну поруку кориснику" + }, + { + "id": "api.command_me.name", + "translation": "ја" + }, + { + "id": "api.command_me.hint", + "translation": "[порука]" + }, + { + "id": "api.command_me.desc", + "translation": "Изврши радњу" + }, + { + "id": "api.command_logout.name", + "translation": "одјава" + }, + { + "id": "api.command_logout.desc", + "translation": "Одјави се са Mattermost-а" + }, + { + "id": "api.command_leave.name", + "translation": "напусти" + }, + { + "id": "api.command_leave.fail.app_error", + "translation": "Догодила се грешка приликом напуштања канала." + }, + { + "id": "api.command_leave.desc", + "translation": "Напусти тренутни канал" + }, + { + "id": "api.command_join.name", + "translation": "приступи" + }, + { + "id": "api.command_join.list.app_error", + "translation": "Догодила се грешка приликом листања канала." + }, + { + "id": "api.command_join.fail.app_error", + "translation": "Догодила се грешка приликом приступања каналу." + }, + { + "id": "api.command_join.desc", + "translation": "Приступи отвореном каналу" + }, + { + "id": "api.command_help.name", + "translation": "помоћ" + }, + { + "id": "api.command_help.desc", + "translation": "Прикажи поруку помоћи Маттермост" + }, + { + "id": "api.command_expand.success", + "translation": "Везе са сликама ће од сада подразумевано бити раширене" + }, + { + "id": "api.command_expand.name", + "translation": "рашири" + }, + { + "id": "api.command_expand.desc", + "translation": "Искључи самоскупљање претпрегледа слика" + }, + { + "id": "api.command_echo.name", + "translation": "одјек" + }, + { + "id": "api.command_echo.message.app_error", + "translation": "Потребно је доставити поруку преко наредбе /одјек." + }, + { + "id": "api.command_echo.hint", + "translation": "'порука' [кашњење у секундама]" + }, + { + "id": "api.command_echo.desc", + "translation": "Направите повратни одјек текста са вашег налога" + }, + { + "id": "api.command_collapse.success", + "translation": "Везе са сликама ће од сада подразумевано бити скупљене" + }, + { + "id": "api.command_collapse.name", + "translation": "скупи" + }, + { + "id": "api.command_collapse.desc", + "translation": "Укључи самоскупљање претпрегледа слика" + }, + { + "id": "api.command_code.name", + "translation": "код" + }, + { + "id": "api.command_code.message.app_error", + "translation": "Потребно је доставити поруку са наредбом /код." + }, + { + "id": "api.command_code.hint", + "translation": "[текст]" + }, + { + "id": "api.command_code.desc", + "translation": "Прикажи текст као блок кода" + }, + { + "id": "api.command_channel_rename.update_channel.app_error", + "translation": "Грешка при ажурирању тренутног канала." + }, + { + "id": "api.command_channel_rename.name", + "translation": "преименуј" + }, + { + "id": "api.command_channel_rename.message.app_error", + "translation": "Потребно је доставити поруку са наредбом /преименуј." + }, + { + "id": "api.command_channel_rename.hint", + "translation": "[текст]" + }, + { + "id": "api.command_channel_rename.desc", + "translation": "Преименуј канал" + }, + { + "id": "api.command_channel_rename.channel.app_error", + "translation": "Грешка при добављању тренутног канала." + }, + { + "id": "api.command_channel_purpose.update_channel.app_error", + "translation": "Грешка при ажурирању сврхе канала." + }, + { + "id": "api.command_channel_purpose.name", + "translation": "сврха" + }, + { + "id": "api.command_channel_purpose.message.app_error", + "translation": "Треба доставити поруку са наредбом /сврха." + }, + { + "id": "api.command_channel_purpose.hint", + "translation": "[текст]" + }, + { + "id": "api.command_channel_purpose.desc", + "translation": "Уреди опис сврхе канала" + }, + { + "id": "api.command_channel_purpose.channel.app_error", + "translation": "Грешка при добављању тренутног канала." + }, + { + "id": "api.command_channel_header.update_channel.app_error", + "translation": "Грешка при ажурирању заглавља канала." + }, + { + "id": "api.command_channel_header.name", + "translation": "заглавље" + }, + { + "id": "api.command_channel_header.message.app_error", + "translation": "Потребно је доставити текст преко наредбе /header." + }, + { + "id": "api.command_channel_header.hint", + "translation": "[текст]" + }, + { + "id": "api.command_channel_header.desc", + "translation": "Уреди заглавље канала" + }, + { + "id": "api.command_channel_header.channel.app_error", + "translation": "Грешка при добављању тренутног канала." + }, + { + "id": "api.command_away.success", + "translation": "Сада сте одсутни" + }, + { + "id": "api.command_away.name", + "translation": "одсутан" + }, + { + "id": "api.command_away.desc", + "translation": "Подесите да сте одсутни" + }, + { + "id": "api.command.invite_people.sent", + "translation": "Мејл позивнице послате" + }, + { + "id": "api.command.invite_people.no_email", + "translation": "Наведите једну или више исправних мејл адреса" + }, + { + "id": "api.command.invite_people.name", + "translation": "позови_особе" + }, + { + "id": "api.command.invite_people.invite_off", + "translation": "Прављење корисника је онемогућено на овом серверу, позивнице нису послате" + }, + { + "id": "api.command.invite_people.hint", + "translation": "[ime@domen.rs ...]" + }, + { + "id": "api.command.invite_people.fail", + "translation": "Догодила се грешка приликом слања мејл позивница" + }, + { + "id": "api.command.invite_people.email_off", + "translation": "Мејл није подешен, нисам послао позивнице" + }, + { + "id": "api.command.invite_people.desc", + "translation": "Пошаљите мејл позивницу вашем Mattermost тиму" + }, + { + "id": "api.command.execute_command.not_found.app_error", + "translation": "Наредба са окидачем '{{.Trigger}}' није нађена. Да бисте послали поруку која почиње са „/“ направите размак на почетку поруке." + }, + { + "id": "api.command.duplicate_trigger.app_error", + "translation": "Ова окидачка реч је већ у употреби. Изаберите другу реч." + }, + { + "id": "api.command.disabled.app_error", + "translation": "Наредбе су онемогућене од стране админа система." + }, + { + "id": "api.command.admin_only.app_error", + "translation": "Интеграције су доступне само админима." + }, + { + "id": "api.channel.update_channel_scheme.license.error", + "translation": "Ваша лиценца не подржава промену шаблона канала" + }, + { + "id": "api.channel.update_channel_privacy.default_channel_error", + "translation": "Подразумевани канал не може бити промењен у приватни." + }, + { + "id": "api.channel.update_channel_member_roles.guest_and_user.app_error", + "translation": "Неисправна промена члана канала: Корисник мора бити гост или корисник али не обоје." + }, + { + "id": "api.channel.remove_member.removed", + "translation": "%v је уклоњен из канала." + }, + { + "id": "api.channel.post_user_add_remove_message_and_forget.error", + "translation": "Нисам успео да објавим поруку приступања или одласка" + }, + { + "id": "api.channel.post_update_channel_header_message_and_forget.updated_to", + "translation": "Корисник %s је изменио заглавље канала у %s" + }, + { + "id": "api.channel.post_update_channel_header_message_and_forget.updated_from", + "translation": "Корисник %s jе изменио заглавље канала из %s у %s" + }, + { + "id": "api.channel.post_update_channel_header_message_and_forget.retrieve_user.error", + "translation": "Нисам успео да добавим корисника приликом ажурирања заглавља канала" + }, + { + "id": "api.channel.post_update_channel_header_message_and_forget.removed", + "translation": "Корисник %s је уклонио заглавље канала (било је: %s)" + }, + { + "id": "api.channel.post_update_channel_header_message_and_forget.post.error", + "translation": "Нисам успео да објавим поруку о промени заглавља канала" + }, + { + "id": "api.channel.post_update_channel_displayname_message_and_forget.updated_from", + "translation": "Корисник %s је изменио приказно име канала из: %s у: %s" + }, + { + "id": "api.channel.post_update_channel_displayname_message_and_forget.retrieve_user.error", + "translation": "Нисам успео да добавим корисника приликом ажурирања поља DisplayName на каналу" + }, + { + "id": "api.channel.post_update_channel_displayname_message_and_forget.create_post.error", + "translation": "Нисам успео да објавим поруку о измени приказног имена" + }, + { + "id": "api.channel.leave.left", + "translation": "%v је напустио канал." + }, + { + "id": "api.channel.leave.last_member.app_error", + "translation": "Ви сте једини преостали члан. Пробајте да уклоните приватни канал уместо напуштања." + }, + { + "id": "api.channel.join_channel.post_and_forget", + "translation": "%v је приступио каналу." + }, + { + "id": "api.channel.guest_join_channel.post_and_forget", + "translation": "Корисник %v се прикључио каналу као гост." + }, + { + "id": "api.channel.delete_channel.type.invalid", + "translation": "Није могуће обрисати канале директних или групних порука" + }, + { + "id": "api.channel.delete_channel.archived", + "translation": "Корисник %v је архивирао канал." + }, + { + "id": "api.channel.create_default_channels.town_square", + "translation": "Градски трг" + }, + { + "id": "api.channel.create_default_channels.off_topic", + "translation": "Ван теме" + }, + { + "id": "api.channel.change_channel_privacy.public_to_private", + "translation": "Овај канал је промењен у Приватни канал." + }, + { + "id": "api.channel.change_channel_privacy.private_to_public", + "translation": "Овај канал је промењен у Јавни канал и може му се прикључити било који члан тима." + }, + { + "id": "api.channel.add_user.to.channel.failed.deleted.app_error", + "translation": "Нисам успео да додам корисника на канал јер је уклоњен од стране тима." + }, + { + "id": "api.channel.add_members.user_denied", + "translation": "Чланство у каналу је одбијено следећим корисницима због групних ограничења: {{ .UserIDs }}" + }, + { + "id": "api.channel.add_members.error", + "translation": "Грешка у додавању корисника канала." + }, + { + "id": "api.channel.add_member.added", + "translation": "%vдодат на канал од %v." + }, + { + "id": "api.channel.add_guest.added", + "translation": "%v додат на канал као гост од стране %v." + }, + { + "id": "api.bot.teams_channels.add_message_mobile", + "translation": "Молим Вас да ме прикључите тиму и каналу у којем желите да учествујем. Да би то урадио, користи броузер или Маттермост десктоп апликацију." + }, + { + "id": "api.bot.create_disabled", + "translation": "Bot creation је онемогућено." + }, + { + "id": "api.admin.upload_brand_image.too_large.app_error", + "translation": "Не могу да отпремим датотеку. Сувише је велика." + }, + { + "id": "api.admin.upload_brand_image.storage.app_error", + "translation": "Не могу да отпремим слику. Складиште слика није подешено." + }, + { + "id": "api.admin.test_s3.missing_s3_bucket", + "translation": "S3 Bucket је нужан" + }, + { + "id": "api.admin.test_email.subject", + "translation": "Mattermost - испробавам подешавања мејлова" + }, + { + "id": "api.admin.test_email.reenter_password", + "translation": "СМТП сервер, порт или корисничко су промењени. Поново унесите СМТП лозинку да бисте испробали везу." + }, + { + "id": "api.admin.test_email.missing_server", + "translation": "Потребан је СМТП сервер" + }, + { + "id": "api.admin.test_email.body", + "translation": "Изгледа да су мејлови у Mattermost-у подешени исправно!" + }, + { + "id": "api.admin.saml.not_available.app_error", + "translation": "SAML 2.0 није подешен или подржан на овом серверу." + }, + { + "id": "api.admin.saml.metadata.app_error", + "translation": "Догодила се грешка приликом изградње мета-података достављача услуга." + }, + { + "id": "api.admin.remove_certificate.delete.app_error", + "translation": "Догодила се грешка при брисању сертификата." + }, + { + "id": "api.admin.get_brand_image.storage.app_error", + "translation": "Складиште за слике није подешено." + }, + { + "id": "api.admin.file_read_error", + "translation": "Грешка при читању записника." + }, + { + "id": "api.admin.delete_brand_image.storage.not_found", + "translation": "Не може да се избрише слика бренда, Није пронађена." + }, + { + "id": "api.admin.add_certificate.saving.app_error", + "translation": "Нисам могао да сачувам датотеку са сертификатом." + }, + { + "id": "api.admin.add_certificate.open.app_error", + "translation": "Нисам могао да отворим датотеку са сертификатом." + }, + { + "id": "api.admin.add_certificate.no_file.app_error", + "translation": "Нема датотеке испод 'certificate' дела у захтеву." + }, + { + "id": "api.admin.add_certificate.array.app_error", + "translation": "Нема фајла под 'certificate' у захтеву." + }, + { + "id": "September", + "translation": "Септембар" + }, + { + "id": "October", + "translation": "Октобар" + }, + { + "id": "November", + "translation": "Новембар" + }, + { + "id": "May", + "translation": "Мај" + }, + { + "id": "March", + "translation": "Март" + }, + { + "id": "June", + "translation": "Јун" + }, + { + "id": "July", + "translation": "Јул" + }, + { + "id": "January", + "translation": "Јануар" + }, + { + "id": "February", + "translation": "Фебруар" + }, + { + "id": "December", + "translation": "Децембар" + }, + { + "id": "August", + "translation": "Август" + }, + { + "id": "April", + "translation": "Април" + }, + { + "id": "api.command_groupmsg.name", + "translation": "порука" + }, + { + "id": "api.admin.saml.set_certificate_from_metadata.missing_content_type.app_error", + "translation": "Недостаје тип садржаја." + }, + { + "id": "api.admin.saml.set_certificate_from_metadata.invalid_content_type.app_error", + "translation": "Тип садржаја није валидан." + }, + { + "id": "api.admin.saml.set_certificate_from_metadata.invalid_body.app_error", + "translation": "Текст сертификата није валидан." + }, + { + "id": "api.admin.ldap.not_available.app_error", + "translation": "Активни директоријум није доступан." + }, + { + "id": "api.admin.add_certificate.parseform.app_error", + "translation": "Грешка у парсирању вишеформног захтева" + }, + { + "id": "Channels", + "translation": "Kanali" + }, + { + "id": "Boards", + "translation": "Table" + } +] diff --git a/server/i18n/sv.json b/server/i18n/sv.json index 9fe7e79296b..d4beeedcc55 100644 --- a/server/i18n/sv.json +++ b/server/i18n/sv.json @@ -77,7 +77,7 @@ }, { "id": "web.error.unsupported_browser.min_browser_version.edge", - "translation": "Version 110+" + "translation": "Version 112+" }, { "id": "web.error.unsupported_browser.min_browser_version.chrome", @@ -319,10 +319,6 @@ "id": "model.file_info.is_valid.create_at.app_error", "translation": "Create_at har ett ogiltigt värde." }, - { - "id": "model.file_info.get.gif.app_error", - "translation": "Kunde inte tolka gif." - }, { "id": "model.emoji.user_id.app_error", "translation": "Ogiltigt \"creator id\"." @@ -9094,10 +9090,6 @@ "id": "api.license.request_trial_license.embargoed", "translation": "Vi kunde inte behandla begäran på grund av begränsningar för länder med embargo. [Läs mer i vår dokumentation] (https://mattermost.com/pl/limitations-for-embargoed-countries), eller kontakta legal@mattermost.com för frågor om exportbegränsningar." }, - { - "id": "api.insights.feature_disabled", - "translation": "Insights ligger bakom en funktionsflagga som inte är aktiverad." - }, { "id": "api.getThreadsForUser.bad_only_params", "translation": "Parametrarna OnlyThreads och OnlyTotals för getThreadsForUser utesluter varandra" @@ -9122,10 +9114,6 @@ "id": "api.elasticsearch.test_elasticsearch_settings_nil.app_error", "translation": "Elasticsearch-inställningarna har odefinierade värden." }, - { - "id": "app.recent_searches.app_error", - "translation": "Fel när senaste sökningar skulle hämtas" - }, { "id": "api.file.cloud_upload.app_error", "translation": "Uppladdning via mmctl till en molninstans stöds inte. Se dokumentationen här: https://docs.mattermost.com/manage/cloud-data-export.html." @@ -9154,18 +9142,6 @@ "id": "model.config.is_valid.image_decoder_concurrency.app_error", "translation": "Ogiltig värde {{.Value}} på antal samtidiga dekoder. Måste vara ett positivt tal eller -1." }, - { - "id": "app.post.get_top_threads_for_user_since.app_error", - "translation": "Det går inte att få fram användarens topp-trådar." - }, - { - "id": "app.post.get_top_threads_for_team_since.app_error", - "translation": "Det går inte att få fram teamets topp-trådar." - }, - { - "id": "app.insights.feature_disabled", - "translation": "Funktionen Insights är inaktiverad." - }, { "id": "api.cloud.notify_admin_to_upgrade_error.already_notified", "translation": "Har redan notifierat systemadministratören" @@ -9198,10 +9174,6 @@ "id": "app.team.clear_cache.app_error", "translation": "Fel vid rensning av cache av teammedlemmar" }, - { - "id": "app.post.get_top_dms_for_user_since.app_error", - "translation": "Det går inte att få fram användarens topp-direktmeddelanden." - }, { "id": "api.command_marketplace.unsupported.app_error", "translation": "Kommandot Marketplace stöds inte på din enhet." @@ -9466,14 +9438,6 @@ "id": "api.team.invite_guests_to_channels.disabled.error", "translation": "Gäståtkomst har inaktiverats" }, - { - "id": "app.collection.add_collection.exists.app_error", - "translation": "Samlingstypen finns redan." - }, - { - "id": "app.collection.add_topic.exists.app_error", - "translation": "Ämnestypen finns redan." - }, { "id": "app.draft.delete.app_error", "translation": "Kunde inte att radera utkastet." @@ -9522,10 +9486,6 @@ "id": "api.acknowledgement.delete.archived_channel.app_error", "translation": "Du kan inte ta bort en bekräftelse i en arkiverad kanal." }, - { - "id": "worktemplate.category.product_teams", - "translation": "Produkt" - }, { "id": "model.draft.is_valid.user_id.app_error", "translation": "Ogiltigt användarid." @@ -9566,14 +9526,6 @@ "id": "model.acknowledgement.is_valid.post_id.app_error", "translation": "Ogiltigt meddelande-id." }, - { - "id": "app.worktemplates.get_templates.app_error", - "translation": "Det går inte att få fram arbetsmallar" - }, - { - "id": "app.worktemplates.get_categories.app_error", - "translation": "Det går inte att få fram kategorier för arbetsmallar" - }, { "id": "app.post_prority.get_for_post.app_error", "translation": "Det går inte att få fram inläggets prioritet" @@ -9690,74 +9642,10 @@ "id": "api.license.true_up_review.create_error", "translation": "Det gick inte att skapa en statuspost för true up" }, - { - "id": "app.worktemplates.execute_work_template.playbooks.find_channel_error", - "translation": "Det går inte att hitta kanalen som är associerad med en playbook." - }, - { - "id": "app.worktemplates.execute_work_template.playbooks.create_error", - "translation": "Fel vid skapandet av en playbook." - }, - { - "id": "app.worktemplates.execute_work_template.channels.create_error", - "translation": "Fel vid skapandet av en kanal." - }, - { - "id": "app.worktemplates.execute_work_template.boards.create_error", - "translation": "Fel vid skapandet av en tavla." - }, - { - "id": "app.worktemplates.execute_work_template.app_error", - "translation": "Fel vid utförandet av en arbetsmall." - }, - { - "id": "app.worktemplate.execution_request.cannot_create_public_playbook", - "translation": "Du har inte behörighet att skapa en publik playbook." - }, - { - "id": "app.worktemplate.execution_request.cannot_create_public_channel", - "translation": "Du har inte behörighet att skapa en publik kanal." - }, - { - "id": "app.worktemplate.execution_request.cannot_create_public_board", - "translation": "Du har inte behörighet att skapa en publik tavla." - }, - { - "id": "app.worktemplate.execution_request.cannot_create_private_playbook", - "translation": "Du har inte behörighet att skapa en privat playbook." - }, - { - "id": "app.worktemplate.execution_request.cannot_create_private_channel", - "translation": "Du har inte behörighet att skapa en privat kanal." - }, - { - "id": "app.worktemplate.execution_request.cannot_create_private_board", - "translation": "Du har inte behörighet att skapa en privat tavla." - }, { "id": "app.group.username_conflict", "translation": "Användaren med användarnamn \"{{.Username}}\" finns redan." }, - { - "id": "api.work_templates.disabled", - "translation": "Arbetsmallar är inaktiverade." - }, - { - "id": "worktemplate.category.leadership", - "translation": "Ledarskap" - }, - { - "id": "worktemplate.category.devops", - "translation": "DevOps" - }, - { - "id": "worktemplate.category.companywide", - "translation": "Företagsgemensam" - }, - { - "id": "app.worktemplate.execution_request.license_cannot_create_private_playbook", - "translation": "Din licens stöder inte privata playbooks." - }, { "id": "app.import.validate_user_teams_import_data.invalid_auth_service.error", "translation": "Ogiltig auth-tjänst: {{.AuthService}}" @@ -9770,46 +9658,6 @@ "id": "api.server.cws.needs_enterprise_edition", "translation": "Tjänsten är endast tillgänglig i Mattermost Enterprise-edition" }, - { - "id": "app.worktemplates.execute_work_template.name_too_long", - "translation": "Fältet Namn får inte innehålla mer än 64 tecken." - }, - { - "id": "app.team.create_onboarding_linked_board.product_key_not_found", - "translation": "hittade inte produktnyckeln" - }, - { - "id": "app.team.create_onboarding_linked_board.itf_not_of_type", - "translation": "gränssnittet är inte av förväntad typ" - }, - { - "id": "app.team.create_onboarding_linked_board.error_patching_board", - "translation": "error när tavlan uppdaterades" - }, - { - "id": "app.team.create_onboarding_linked_board.error_no_board", - "translation": "fel, ingen tavla skapades" - }, - { - "id": "app.team.create_onboarding_linked_board.error_getting_templates", - "translation": "fel när mallar för tavlor hämtades" - }, - { - "id": "app.team.create_onboarding_linked_board.error_duplicating_board", - "translation": "fel när tavlan duplicerades" - }, - { - "id": "api.command_templates.unsupported.app_error", - "translation": "Kommandot för mallar stöds inte på din enhet." - }, - { - "id": "api.command_templates.name", - "translation": "mallar" - }, - { - "id": "api.command_templates.desc", - "translation": "Öppna fönstret Skapa från mall" - }, { "id": "api.system.logs.invalidFilter", "translation": "Felaktigt logg-filter" @@ -9826,139 +9674,6 @@ "id": "app.oauth.remove_auth_data_by_client_id.app_error", "translation": "Kunde inte rensa oauth-information." }, - { - "id": "app.user.run.update_status.title", - "translation": "Statusuppdatering" - }, - { - "id": "app.user.run.update_status.submit_label", - "translation": "Uppdatera status" - }, - { - "id": "app.user.run.update_status.reminder_for_next_update", - "translation": "Påminnelse om nästa uppdatering" - }, - { - "id": "app.user.run.update_status.num_channel", - "translation": { - "one": "Ge en uppdatering till intressenterna. Detta inlägg kommer att publiceras i {{.Count}} kanal.", - "other": "Ge en uppdatering till intressenterna. Detta inlägg kommer att publiceras i {{.Count}} kanaler." - } - }, - { - "id": "app.user.run.update_status.finish_run.placeholder", - "translation": "Markera även körningen som avslutad" - }, - { - "id": "app.user.run.update_status.finish_run", - "translation": "Slutför körningen" - }, - { - "id": "app.user.run.update_status.change_since_last_update", - "translation": "Förändring sedan den senaste uppdateringen" - }, - { - "id": "app.user.run.status_enable", - "translation": "@{{.Username}} aktiverade statusuppdateringar för [{{.RunName}}]({{.RunURL}})" - }, - { - "id": "app.user.run.status_disable", - "translation": "@{{.Username}} inaktiverade statusuppdateringarna för [{{.RunName}}]({{.RunURL}})" - }, - { - "id": "app.user.run.request_update", - "translation": "@here - @{{.Name}} begärde en statusuppdatering för [{{.RunName}}]({{.RunURL}}). \n" - }, - { - "id": "app.user.run.request_join_channel", - "translation": "@{{.Name}} är en deltagare i en körning och vill gå med i den här kanalen. Alla medlemmar i kanalen kan bjuda in dem.\n" - }, - { - "id": "app.user.run.confirm_finish.title", - "translation": "Bekräfta att avsluta körningen" - }, - { - "id": "app.user.run.confirm_finish.submit_label", - "translation": "Slutför körningen" - }, - { - "id": "app.user.run.confirm_finish.num_outstanding", - "translation": { - "one": "Det finns **{{.Count}} utestående uppgift**. Är du säker på att du vill avsluta körningen *{{.RunName}}* för alla deltagare?", - "other": "Det finns **{{.Count}} utestående uppgifter**. Är du säker på att du vill avsluta körningen *{{.RunName}}* för alla deltagare?" - } - }, - { - "id": "app.user.run.add_to_timeline.title", - "translation": "Lägg till i tidslinjen för körning" - }, - { - "id": "app.user.run.add_to_timeline.summary.placeholder", - "translation": "Kort sammanfattning som visas i tidslinjen" - }, - { - "id": "app.user.run.add_to_timeline.summary.help", - "translation": "Max 64 tecken" - }, - { - "id": "app.user.run.add_to_timeline.summary", - "translation": "Sammanfattning" - }, - { - "id": "app.user.run.add_to_timeline.submit_label", - "translation": "Lägg till i tidslinjen för körning" - }, - { - "id": "app.user.run.add_to_timeline.playbook_run", - "translation": "Kör Playbook" - }, - { - "id": "app.user.run.add_checklist_item.title", - "translation": "Lägg till en ny uppgift" - }, - { - "id": "app.user.run.add_checklist_item.submit_label", - "translation": "Lägg till en uppgift" - }, - { - "id": "app.user.run.add_checklist_item.name", - "translation": "Namn" - }, - { - "id": "app.user.run.add_checklist_item.description", - "translation": "Beskrivning" - }, - { - "id": "app.user.new_run.title", - "translation": "Kör playbook" - }, - { - "id": "app.user.new_run.submit_label", - "translation": "Starta körning" - }, - { - "id": "app.user.new_run.run_name", - "translation": "Namn på körning" - }, - { - "id": "app.user.new_run.playbook", - "translation": "Playbook" - }, - { - "id": "app.user.new_run.intro", - "translation": "**Ägare** {{.Username}}" - }, - { - "id": "app.user.digest.tasks.zero_assigned", - "translation": "Du har 0 tilldelade uppgifter." - }, - { - "id": "app.user.digest.tasks.num_assigned_due_until_today", - "translation": { - "one": "Du har {{.Count}} tilldelad uppgift som nu är förfallen:", - "other": "Du har {{.Count}} tilldelade uppgifter som nu är förfallna:" - } - }, { "id": "app.user.digest.tasks.num_assigned", "translation": { @@ -9966,106 +9681,6 @@ "other": "Du har {{.Count}} tilldelade uppgifter:" } }, - { - "id": "app.user.digest.tasks.heading", - "translation": "Dina tilldelade uppgifter" - }, - { - "id": "app.user.digest.tasks.due_yesterday", - "translation": "Skulle utförts igår" - }, - { - "id": "app.user.digest.tasks.due_x_days_ago", - "translation": "Försenad {{.Count}} dagar" - }, - { - "id": "app.user.digest.tasks.due_in_x_days", - "translation": { - "one": "Ska utföras inom {{.Count}} dag", - "other": "Ska utföras inom {{.Count}} dagar" - } - }, - { - "id": "app.user.digest.tasks.due_today", - "translation": "Ska utföras idag" - }, - { - "id": "app.user.digest.tasks.due_after_today", - "translation": { - "one": "Du har **{{.Count}} tilldelad uppgift som ska utföras idag**.", - "other": "Du har **{{.Count}} tilldelade uppgifter som ska utföras idag**." - } - }, - { - "id": "app.user.digest.tasks.all_tasks_command", - "translation": "Använd `/playbook todo` för att se alla dina uppgifter." - }, - { - "id": "app.user.digest.runs_in_progress.zero_in_progress", - "translation": "Du har 0 pågående körningar." - }, - { - "id": "app.user.digest.runs_in_progress.num_in_progress", - "translation": { - "one": "Du har {{.Count}} körning som för närvarande pågår:", - "other": "Du har {{.Count}} körningar som för närvarande pågår:" - } - }, - { - "id": "app.user.digest.runs_in_progress.heading", - "translation": "Körningar som pågår" - }, - { - "id": "app.user.digest.overdue_status_updates.zero_overdue", - "translation": "Du har 0 försenade körningar." - }, - { - "id": "app.user.digest.overdue_status_updates.num_overdue", - "translation": { - "one": "Du har {{.Count}} körning som borde ha fått en statusuppdatering:", - "other": "Du har {{.Count}} körningar som borde ha fått en statusuppdatering:" - } - }, - { - "id": "app.user.digest.overdue_status_updates.heading", - "translation": "Status om förseningar" - }, - { - "id": "app.command.execute.error", - "translation": "Kunde inte utföra kommandot." - }, - { - "id": "worktemplate.devops.incident_resolution.description.channel", - "translation": "Chatta med ditt team om prioriteringar, lägg till intressenter, ge uppdateringar och arbeta mot en lösning i en och samma kanal." - }, - { - "id": "worktemplate.devops.incident_resolution.description.board", - "translation": "Använd Incident Resolution tavlan för att stödja upprepbara processer och fördela definierade uppgifter i hela teamet." - }, - { - "id": "worktemplate.companywide.goals_and_okrs.integration", - "translation": "Öka produktiviteten i din kanal genom att integrera dina mest använda verktyg, exempelvis Zoom, för att underlätta samarbetet. Dessa kommer att laddas ner åt dig." - }, - { - "id": "worktemplate.companywide.goals_and_okrs.channel", - "translation": "Chatta om dina mål och framsteg med ditt team, asynkront eller i realtid, och håll dig uppdaterad om förändringar i en och samma kanal." - }, - { - "id": "worktemplate.companywide.goals_and_okrs.board", - "translation": "Håll koll på teamets steg mot de organisatoriska målen med OKR-tavlan. Håll möten på rätt spår med hjälp av Mötesagenda-tavlan." - }, - { - "id": "worktemplate.companywide.create_project.integration", - "translation": "Öka produktiviteten i din kanal genom att integrera dina mest använda verktyg. Dessa kommer att laddas ner åt dig." - }, - { - "id": "worktemplate.companywide.create_project.channel", - "translation": "Chatta med ditt team om ditt nya projekt och bestäm hur ni ska strukturera det i en samarbetskanal." - }, - { - "id": "worktemplate.companywide.create_project.board", - "translation": "Använd en Kanban-tavla för att definiera och hålla koll på dina uppgifter och framsteg i projekt." - }, { "id": "model.license_record.is_valid.bytes.app_error", "translation": "Felaktigt värde för bytes licensen laddas upp." @@ -10078,10 +9693,6 @@ "id": "app.plugin.skip_installation.app_error", "translation": "Hoppar över installationen av plugin {{.Id}} eftersom den befintliga versionen är samma eller nyare." }, - { - "id": "app.plugin.blocked.app_error", - "translation": "Plugin {{.Id}} finns på blocklistan. Vissa plugins är blockerade eftersom de är inbyggda i den här versionen av Mattermost." - }, { "id": "api.server.cws.subscribe_to_newsletter.app_error", "translation": "CWS Server misslyckades med att prenumerera på nyhetsbrev." @@ -10097,5 +9708,173 @@ { "id": "api.error_no_organization_name_provided_for_self_hosted_onboarding", "translation": "Fel: inget organisationsnamn har angetts för egen onboarding." + }, + { + "id": "app.file_info.get.gif.app_error", + "translation": "Kunde inte avkoda gif." + }, + { + "id": "api.user.invalidate_password_recovery_tokens_parse.error", + "translation": "Det går inte att tolka token när lösenordsåterställningstoken är ogiltiga" + }, + { + "id": "app.post_persistent_notification.delete_by_team.app_error", + "translation": "Kunde inte radera persistenta notifieringar per team." + }, + { + "id": "api.post.post_priority.urgent_persistent_notification_post.request_error", + "translation": "Persistenta notifieringar måste ha prioriteringen Brådskande." + }, + { + "id": "app.post.delete_post.get_team.app_error", + "translation": "Ett fel uppstod när team skulle hämtas." + }, + { + "id": "app.post_persistent_notification.delete_by_channel.app_error", + "translation": "Kunde inte radera persistenta notifieringar per kanal." + }, + { + "id": "model.config.is_valid.persistent_notifications_recipients.app_error", + "translation": "Ogiltigt maximalt antal mottagare för permanenta notifieringar. Måste vara ett positivt tal." + }, + { + "id": "app.eport.generate_presigned_url.config.app_error", + "translation": "Denna åtgärd kräver användning av specifik lagring av export." + }, + { + "id": "app.eport.generate_presigned_url.featureflag.app_error", + "translation": "Denna funktion begränsas av en funktionsflagga." + }, + { + "id": "app.eport.generate_presigned_url.link.app_error", + "translation": "Kan inte generera presigned url." + }, + { + "id": "app.eport.generate_presigned_url.fileexist.app_error", + "translation": "Kunde inte kontrollera om filen finns." + }, + { + "id": "api.user.invalidate_password_recovery_tokens.error", + "translation": "Det går inte att hämta tokens efter typ när lösenordsåterställningstoken är ogiltiga" + }, + { + "id": "model.config.is_valid.persistent_notifications_count.app_error", + "translation": "Ogiltigt värde på persistent notifieringar per meddelande. Måste vara ett positivt tal." + }, + { + "id": "api.command_exportlink.desc", + "translation": "Skapa en länk för att hämta en export." + }, + { + "id": "api.command_exportlink.driver.app_error", + "translation": "File store-drivrutinen stöder inte länkgenerering." + }, + { + "id": "api.command_exportlink.empty.app_error", + "translation": "Ingen exportfil har hittats." + }, + { + "id": "api.command_exportlink.hint", + "translation": "[jobb-id|zip filnamn|{{.LatestMsg}}]" + }, + { + "id": "api.command_exportlink.invalid.app_error", + "translation": "Det gick inte att hitta den fil du efterfrågade." + }, + { + "id": "api.command_exportlink.link.text", + "translation": "Du kan ladda ner din fil här: {{.Länk}}.\nDenna länk kommer att upphöra att gälla om {{.Expiration}}." + }, + { + "id": "api.command_exportlink.name", + "translation": "exportlänk" + }, + { + "id": "api.command_exportlink.list.app_error", + "translation": "Det gick inte att hämta listan med exporter." + }, + { + "id": "api.command_exportlink.permission.app_error", + "translation": "Du har inte tillräckliga behörigheter för att köra kommandot." + }, + { + "id": "api.command_exportlink.presign.app_error", + "translation": "Kan inte generera presign url." + }, + { + "id": "app.channel.get_channels_member_count.existing.app_error", + "translation": "Kan inte hitta medlemsantal för givna kanaler." + }, + { + "id": "app.channel.get_channels_member_count.find.app_error", + "translation": "Kan inte hitta antalet medlemmar." + }, + { + "id": "app.channel.update_member.app_error", + "translation": "Kan inte uppdatera kanalmedlemmen." + }, + { + "id": "app.channel.update_member.notify_props_limit_exceeded.app_error", + "translation": "Kan inte uppdatera kanalmedlemmen, storleksgränsen för notify props har överskridits." + }, + { + "id": "app.eport.generate_presigned_url.driver.app_error", + "translation": "Din drivrutin för lagring av export har inte stöd för generering av presign url." + }, + { + "id": "app.eport.generate_presigned_url.notfound.app_error", + "translation": "Exportfilen hittades inte." + }, + { + "id": "app.post_priority.delete_persistent_notification_post.app_error", + "translation": "Kunde inte ta bort persistent notiferingspost" + }, + { + "id": "app.save_config.plugin_hook_error", + "translation": "Ett fel uppstod när plugin-hooken kördes när konfigurationen sparades." + }, + { + "id": "model.channel_member.is_valid.notify_props.app_error", + "translation": "Notify props storleksgräns har överskridits." + }, + { + "id": "model.config.is_valid.elastic_search.ignored_indexes_dash_prefix.app_error", + "translation": "Ignorerade index för rensning ska inte börja med bindestreck." + }, + { + "id": "model.config.is_valid.persistent_notifications_interval.app_error", + "translation": "Ogiltigt värde på persistent notifiering. Måste vara minst två minuter." + }, + { + "id": "api.user.invalidate_password_recovery_tokens_delete.error", + "translation": "Det går inte att ta bort token när lösenordsåterställningstoken är ogiltiga" + }, + { + "id": "model.channel_member.is_valid.channel_auto_follow_threads_value.app_error", + "translation": "Ogiltigt värde för channel-auto-follow-threads." + }, + { + "id": "model.config.is_valid.local_mode_socket.app_error", + "translation": "Det gick inte att hitta katalogen för lokal socket-fil." + }, + { + "id": "api.post.post_priority.max_recipients_persistent_notification_post.request_error", + "translation": "Meddelanden med persistenta notifieringar tillåter maximalt {{.MaxRecipients}} mottagare." + }, + { + "id": "api.post.post_priority.min_recipients_persistent_notification_post.request_error", + "translation": "Meddelanden med persistenta notifieringar måste ha minst 1 mottagare." + }, + { + "id": "api.post.post_priority.persistent_notification_validation_error.request_error", + "translation": "Validering av persistenta notifieringar misslyckades." + }, + { + "id": "api.post.post_priority.priority_post_not_allowed_for_user.request_error", + "translation": "Användaren får inte skapa prioriterade inlägg eller persistenta notifieringar." + }, + { + "id": "api.post.post_priority.priority_post_only_allowed_for_root_post.request_error", + "translation": "Endast rot-meddelanden tillåts ha prioritering." } ] diff --git a/server/i18n/th.json b/server/i18n/th.json new file mode 100644 index 00000000000..7449d0f034e --- /dev/null +++ b/server/i18n/th.json @@ -0,0 +1,50 @@ +[ + { + "id": "September", + "translation": "กันยายน" + }, + { + "id": "October", + "translation": "ตุลาคม" + }, + { + "id": "November", + "translation": "พฤศจิกายน" + }, + { + "id": "May", + "translation": "พฤษภาคม" + }, + { + "id": "March", + "translation": "มีนาคม" + }, + { + "id": "June", + "translation": "มิถุนายน" + }, + { + "id": "July", + "translation": "กรกฎาคม" + }, + { + "id": "January", + "translation": "มกราคม" + }, + { + "id": "February", + "translation": "กุมภาพันธ์" + }, + { + "id": "December", + "translation": "ธันวาคม" + }, + { + "id": "August", + "translation": "สิงหาคม" + }, + { + "id": "April", + "translation": "เมษายน" + } +] diff --git a/server/i18n/tr.json b/server/i18n/tr.json index e89bf2c462a..08b70601e0f 100644 --- a/server/i18n/tr.json +++ b/server/i18n/tr.json @@ -812,27 +812,27 @@ }, { "id": "api.command_offline.desc", - "translation": "Durumunuzu çevrimdışı yapar" + "translation": "Durumunuzu çevrim dışı yapar" }, { "id": "api.command_offline.name", - "translation": "çevrimdışı" + "translation": "çevrim dışı" }, { "id": "api.command_offline.success", - "translation": "Durumunuz: çevrimdışı" + "translation": "Durumunuz: Çevrim dışı" }, { "id": "api.command_online.desc", - "translation": "Durumunuzu çevrimiçi yapar" + "translation": "Durumunuzu çevrim içi yapar" }, { "id": "api.command_online.name", - "translation": "çevrimiçi" + "translation": "çevrim içi" }, { "id": "api.command_online.success", - "translation": "Durumunuz: çevrimiçi" + "translation": "Durumunuz: Çevrim içi" }, { "id": "api.command_open.name", @@ -1111,7 +1111,7 @@ }, { "id": "api.incoming_webhook.disabled.app_error", - "translation": "Gelen web bağlantıları sistem yöneticisi tarafından devre dışı bırakılmış." + "translation": "Gelen internet kancaları sistem yöneticisi tarafından devre dışı bırakılmış." }, { "id": "api.incoming_webhook.invalid_username.app_error", @@ -1211,19 +1211,19 @@ }, { "id": "api.oauth.get_access_token.internal.app_error", - "translation": "Sunucu sorunu: Veritabanına erişilirken bir iç sunucu sorunu çıktı." + "translation": "Sunucu sorunu: Veri tabanına erişilirken bir iç sunucu sorunu çıktı." }, { "id": "api.oauth.get_access_token.internal_saving.app_error", - "translation": "Sunucu sorunu: Erişim kodu veritabanına kaydedilirken bir iç sunucu sorunu çıktı." + "translation": "Sunucu sorunu: Erişim kodu veri tabanına kaydedilirken bir iç sunucu sorunu çıktı." }, { "id": "api.oauth.get_access_token.internal_session.app_error", - "translation": "Sunucu sorunu: Oturum veritabanına kaydedilirken bir iç sunucu sorunu çıktı." + "translation": "Sunucu sorunu: Oturum veri tabanına kaydedilirken bir iç sunucu sorunu çıktı." }, { "id": "api.oauth.get_access_token.internal_user.app_error", - "translation": "Sunucu sorunu: Kullanıcı veritabanından alınırken bir iç sunucu sorunu çıktı." + "translation": "Sunucu sorunu: Kullanıcı veri tabanından alınırken bir iç sunucu sorunu çıktı." }, { "id": "api.oauth.get_access_token.missing_code.app_error", @@ -1251,15 +1251,15 @@ }, { "id": "api.oauth.revoke_access_token.del_session.app_error", - "translation": "Oturum veritabanından silinirken sorun çıktı." + "translation": "Oturum veri tabanından silinirken sorun çıktı." }, { "id": "api.oauth.revoke_access_token.del_token.app_error", - "translation": "Erişim kodu veritabanından silinirken sorun çıktı." + "translation": "Erişim kodu veri tabanından silinirken sorun çıktı." }, { "id": "api.oauth.revoke_access_token.get.app_error", - "translation": "Silinmeden önce erişim kodu veritabanından alınırken sorun çıktı." + "translation": "Silinmeden önce erişim kodu veri tabanından alınırken sorun çıktı." }, { "id": "api.oauth.singup_with_oauth.disabled.app_error", @@ -1275,7 +1275,7 @@ }, { "id": "api.outgoing_webhook.disabled.app_error", - "translation": "Giden web bağlantıları sistem yöneticisi tarafından devre dışı bırakılmış." + "translation": "Giden internet kancaları sistem yöneticisi tarafından devre dışı bırakılmış." }, { "id": "api.plugin.upload.array.app_error", @@ -1481,7 +1481,7 @@ }, { "id": "api.server.start_server.forward80to443.enabled_but_listening_on_wrong_port", - "translation": "%s kapısı dinlenirken 80 numaralı kapı 443 numaralı kapıya yönlendirilemedi. Bir vekil sunucu kullanıyorsanız Forward80To443 seçeneğini devre dışı bırakın" + "translation": "%s bağlantı noktası dinlenirken 80 numaralı bağlantı noktası 443 numaralı bağlantı noktasına yönlendirilemedi. Bir vekil sunucu kullanıyorsanız Forward80To443 seçeneğini devre dışı bırakın" }, { "id": "api.server.start_server.rate_limiting_memory_store", @@ -2281,15 +2281,15 @@ }, { "id": "api.webhook.create_outgoing.intersect.app_error", - "translation": "Aynı kanaldaki giden web bağlantılarında aynı tetikleyici sözcük ve geri çağırma adresleri bulunamaz." + "translation": "Aynı kanaldaki giden internet kancalarında aynı tetikleyici sözcük ve geri çağırma adresleri bulunamaz." }, { "id": "api.webhook.create_outgoing.not_open.app_error", - "translation": "Giden web bağlantıları yalnızca herkese açık kanallara eklenebilir." + "translation": "Giden internet kancaları yalnızca herkese açık kanallara eklenebilir." }, { "id": "api.webhook.create_outgoing.permissions.app_error", - "translation": "Giden web bağlantısı eklemek için izinleriniz yetersiz." + "translation": "Giden internet kancası eklemek için izinleriniz yetersiz." }, { "id": "api.webhook.create_outgoing.triggers.app_error", @@ -2297,15 +2297,15 @@ }, { "id": "api.webhook.incoming.error", - "translation": "Gelen web bağlantsının birden çok parçalı paketinin kodu çözülemedi." + "translation": "Gelen internet kancasının birkaç parçalı paketinin kodu çözülemedi." }, { "id": "api.webhook.team_mismatch.app_error", - "translation": "Web bağlantısı takımlar arasında güncellenemez." + "translation": "İnternet kancası takımlar arasında güncellenemez." }, { "id": "api.webhook.update_outgoing.intersect.app_error", - "translation": "Aynı kanaldaki giden web bağlantılarında aynı tetikleyici sözcük ve geri çağırma adresleri bulunamaz." + "translation": "Aynı kanaldaki giden internet kancalarında aynı tetikleyici sözcük ve geri çağırma adresleri bulunamaz." }, { "id": "api.websocket_handler.invalid_param.app_error", @@ -4009,11 +4009,11 @@ }, { "id": "model.config.is_valid.webserver_security.app_error", - "translation": "Web sunucusu bağlantı güvenliği değeri geçersiz." + "translation": "Site sunucusu bağlantı güvenliği değeri geçersiz." }, { "id": "model.config.is_valid.websocket_url.app_error", - "translation": "Web soketi adresi ws:// ya da wss:// ile başlayan geçerli bir adres olmalıdır." + "translation": "WebSocket adresi ws:// ya da wss:// ile başlayan geçerli bir adres olmalıdır." }, { "id": "model.config.is_valid.write_timeout.app_error", @@ -4039,10 +4039,6 @@ "id": "model.emoji.user_id.app_error", "translation": "Ekleyen kodu geçersiz." }, - { - "id": "model.file_info.get.gif.app_error", - "translation": "Gif kodu çözülemedi." - }, { "id": "model.file_info.is_valid.create_at.app_error", "translation": "Eklenme zamanı değeri geçersiz." @@ -4661,15 +4657,15 @@ }, { "id": "web.incoming_webhook.channel_locked.app_error", - "translation": "Bu web bağlantısının istenilen kanala gönderilmesine izin verilmiyor." + "translation": "Bu internet bağlantısının istenilen kanala gönderilmesine izin verilmiyor." }, { "id": "web.incoming_webhook.disabled.app_error", - "translation": "Gelen web bağlantıları sistem yöneticisi tarafından devre dışı bırakılmış." + "translation": "Gelen internet kancaları sistem yöneticisi tarafından devre dışı bırakılmış." }, { "id": "web.incoming_webhook.invalid.app_error", - "translation": "Web bağlantısı geçersiz." + "translation": "İnternet kancası geçersiz." }, { "id": "web.incoming_webhook.parse.app_error", @@ -4681,7 +4677,7 @@ }, { "id": "web.incoming_webhook.split_props_length.app_error", - "translation": "Web bağlantısı bölümleri {{.Max}} karakterlik parçalara ayrılamadı." + "translation": "İnternet kancası bölümleri {{.Max}} karakterlik parçalara ayrılamadı." }, { "id": "web.incoming_webhook.text.app_error", @@ -4829,7 +4825,7 @@ }, { "id": "app.plugin.webapp_bundle.app_error", - "translation": "Uygulama eki web uygulaması paketi oluşturulamadı." + "translation": "Uygulama eki internet uygulaması paketi oluşturulamadı." }, { "id": "app.plugin.write_file.read.app_error", @@ -4913,7 +4909,7 @@ }, { "id": "bleveengine.indexer.do_job.get_oldest_entity.error", - "translation": "Veritabanından en eski varlık (kullanıcı, kanal ya da ileti) alınamadı." + "translation": "Veri tabanından en eski varlık (kullanıcı, kanal ya da ileti) alınamadı." }, { "id": "bleveengine.indexer.do_job.parse_start_time.error", @@ -5149,7 +5145,7 @@ }, { "id": "api.bot.teams_channels.add_message_mobile", - "translation": "Lütfen beni etkileşimde bulunmamı istediğiniz takım ve kanallara ekleyin. Bu işlemi web tarayıcı ya da Mattermost Masaüstü uygulaması üzerinden yapabilirsiniz." + "translation": "Lütfen beni etkileşimde bulunmamı istediğiniz takım ve kanallara ekleyin. Bu işlemi tarayıcı ya da Mattermost masaüstü uygulaması üzerinden yapabilirsiniz." }, { "id": "api.channel.channel_member_counts_by_group.license.error", @@ -5457,11 +5453,11 @@ }, { "id": "web.error.unsupported_browser.download_app_or_upgrade_browser", - "translation": "Mattermost uygulamasını indirin ya da daha iyi bir deneyim elde etmek için desteklenen bir web tarayıcı kullanın." + "translation": "Mattermost uygulamasını indirin ya da daha iyi bir deneyim elde etmek için desteklenen bir tarayıcı kullanın." }, { "id": "web.error.unsupported_browser.no_longer_support", - "translation": "Bu web tarayıcı artık Mattermost tarafından desteklenmiyor" + "translation": "Bu tarayıcı artık Mattermost tarafından desteklenmiyor" }, { "id": "api.push_notifications.session.expired", @@ -5833,15 +5829,15 @@ }, { "id": "web.error.unsupported_browser.learn_more", - "translation": "Desteklenen web tarayıcılarını öğrenin." + "translation": "Desteklenen tarayıcıları öğrenin." }, { "id": "web.error.unsupported_browser.min_browser_version.chrome", - "translation": "Sürüm 110+" + "translation": "Sürüm 112+" }, { "id": "web.error.unsupported_browser.min_browser_version.edge", - "translation": "Sürüm 110+" + "translation": "Sürüm 112+" }, { "id": "web.error.unsupported_browser.min_browser_version.firefox", @@ -5861,7 +5857,7 @@ }, { "id": "web.error.unsupported_browser.no_longer_support_version", - "translation": "Kullandığınız web tarayıcının bu sürümü artık Mattermost tarafından desteklenmiyor" + "translation": "Kullandığınız tarayıcının bu sürümü artık Mattermost tarafından desteklenmiyor" }, { "id": "web.error.unsupported_browser.open_system_browser.edge", @@ -5981,11 +5977,11 @@ }, { "id": "app.command_webhook.try_use.invalid", - "translation": "Web bağlantısı geçersiz." + "translation": "İnternet kancası geçersiz." }, { "id": "app.command_webhook.try_use.internal_error", - "translation": "Web bağlantısı kullanılamaz." + "translation": "İnternet kancası kullanılamaz." }, { "id": "app.command_webhook.handle_command_webhook.parse", @@ -5993,19 +5989,19 @@ }, { "id": "app.command_webhook.get.missing", - "translation": "Web bağlantısı bulunamadı." + "translation": "İnternet kancası bulunamadı." }, { "id": "app.command_webhook.get.internal_error", - "translation": "Web bağlantısı alınamadı." + "translation": "İnternet kancası alınamadı." }, { "id": "app.command_webhook.create_command_webhook.internal_error", - "translation": "KomutWebBağlantısı kaydedilemedi." + "translation": "CommandWebhook kaydedilemedi." }, { "id": "app.command_webhook.create_command_webhook.existing", - "translation": "Var olan bir KomutWebBağlantısını güncelleyemezsiniz." + "translation": "Var olan bir KomutİnternetKancasını güncelleyemezsiniz." }, { "id": "app.oauth.update_app.updating.app_error", @@ -6357,83 +6353,83 @@ }, { "id": "app.webhooks.delete_incoming.app_error", - "translation": "Web bağlantısı silinemedi." + "translation": "İnternet kancası silinemedi." }, { "id": "app.webhooks.analytics_outgoing_count.app_error", - "translation": "Giden web bağlantıları sayılamadı." + "translation": "Giden internet kancaları sayılamadı." }, { "id": "app.webhooks.analytics_incoming_count.app_error", - "translation": "Gelen web bağlantıları sayılamadı." + "translation": "Gelen internet kancaları sayılamadı." }, { "id": "app.webhooks.update_outgoing.app_error", - "translation": "Web bağlantısı güncellenemedi." + "translation": "İnternet kancası güncellenemedi." }, { "id": "app.webhooks.update_incoming.app_error", - "translation": "Gelen web bağlantısı güncellenemedi." + "translation": "Gelen internet kancası güncellenemedi." }, { "id": "app.webhooks.save_outgoing.override.app_error", - "translation": "Var olan bir giden web bağlantısının üzerine yazılamaz." + "translation": "Var olan bir giden internet kancasının üzerine yazılamaz." }, { "id": "app.webhooks.save_outgoing.app_error", - "translation": "Giden web bağlantısı kaydedilemedi." + "translation": "Giden internet kancası kaydedilemedi." }, { "id": "app.webhooks.save_incoming.existing.app_error", - "translation": "Var olan bir gelen web bağlantısının üzerine yazılamaz." + "translation": "Var olan bir gelen internet kancasının üzerine yazılamaz." }, { "id": "app.webhooks.save_incoming.app_error", - "translation": "Gelen web bağlantısı kaydedilemedi." + "translation": "Gelen internet kancası kaydedilemedi." }, { "id": "app.webhooks.permanent_delete_outgoing_by_user.app_error", - "translation": "Web bağlantısı silinemedi." + "translation": "İnternet kancası silinemedi." }, { "id": "app.webhooks.permanent_delete_outgoing_by_channel.app_error", - "translation": "Web bağlantısı silinemedi." + "translation": "İnternet kancası silinemedi." }, { "id": "app.webhooks.permanent_delete_incoming_by_user.app_error", - "translation": "Web bağlantısı silinemedi." + "translation": "İnternet kancası silinemedi." }, { "id": "app.webhooks.permanent_delete_incoming_by_channel.app_error", - "translation": "Web bağlantısı silinemedi." + "translation": "İnternet kancası silinemedi." }, { "id": "app.webhooks.get_outgoing_by_team.app_error", - "translation": "Web bağlantıları alınamadı." + "translation": "İnternet kancaları alınamadı." }, { "id": "app.webhooks.get_outgoing_by_channel.app_error", - "translation": "Web bağlantıları alınamadı." + "translation": "İnternet kancaları alınamadı." }, { "id": "app.webhooks.get_outgoing.app_error", - "translation": "Web bağlantısı alınamadı." + "translation": "İnternet kancası alınamadı." }, { "id": "app.webhooks.get_incoming_by_user.app_error", - "translation": "Web bağlantısı alınamadı." + "translation": "İnternet kancası alınamadı." }, { "id": "app.webhooks.get_incoming_by_channel.app_error", - "translation": "Web bağlantıları alınamadı." + "translation": "İnternet kancaları alınamadı." }, { "id": "app.webhooks.get_incoming.app_error", - "translation": "Web bağlantısı alınamadı." + "translation": "İnternet kancası alınamadı." }, { "id": "app.webhooks.delete_outgoing.app_error", - "translation": "Web bağlantısı silinemedi." + "translation": "İnternet kancası silinemedi." }, { "id": "app.role.save.invalid_role.app_error", @@ -6733,11 +6729,11 @@ }, { "id": "api.user.login_cws.license.error", - "translation": "CWS oturumu açmaya izin verilmiyor." + "translation": "Müşteri site sunucusu oturum açmaya izin vermiyor." }, { "id": "api.user.login_by_cws.invalid_token.app_error", - "translation": "CWS kodu geçersiz" + "translation": "Müşteri site sunucusu kodu geçersiz" }, { "id": "api.user.get_uploads_for_user.forbidden.app_error", @@ -6785,7 +6781,7 @@ }, { "id": "app.channel.sidebar_categories.app_error", - "translation": "Kayıt veritabanına eklenemedi." + "translation": "Kayıt veri tabanına eklenemedi." }, { "id": "app.channel.get_members.app_error", @@ -7197,7 +7193,7 @@ }, { "id": "api.cloud.request_error", - "translation": "CWS isteği işlenirken sorun çıktı." + "translation": "Müşteri site sunucusu isteği işlenirken sorun çıktı." }, { "id": "model.plugin_command_error.error.app_error", @@ -7217,7 +7213,7 @@ }, { "id": "api.server.warn_metric.number_of_posts_2M.start_trial.notification_body", - "translation": "Mattermost sisteminizde çok sayıda ileti var. Varsayılan Mattermost veritabanı aramasının başarımı 2.5 milyon iletiye yaklaşıldığında düşmeye başlar. 5 milyondan fazla iletide Elasticsearch kullanmak, arama ve anma durumlarında zaman aşımları gibi önemli başarım sorunlarını önleyebilir. Ayrıntılı bilgi almak ve size nasıl yardımcı olabileceğimizi anlatmak için bizimle görüşün.\n\n[Başarımı arttırmak hakkında ayrıntılı bilgi alın](https://www.mattermost.com/docs-elasticsearch/?utm_medium=product&utm_source=mattermost-advisor-bot&utm_content=elasticsearch)\n\nDenemeyi başlat üzerine tıklayarak [Mattermost yazılım deneme sözleşmesi](https://mattermost.com/software-evaluation-agreement/) ve [Kişisel verilerin gizliliği ilkesi](https://mattermost.com/pl/privacy-policy/) metinleri ile ürün tanıtımı e-postalarını almayı kabul ediyorum." + "translation": "Mattermost sisteminizde çok sayıda ileti var. Varsayılan Mattermost veri tabanı aramasının başarımı 2.5 milyon iletiye yaklaşıldığında düşmeye başlar. 5 milyondan fazla iletide Elasticsearch kullanmak, arama ve anma durumlarında zaman aşımları gibi önemli başarım sorunlarını önleyebilir. Ayrıntılı bilgi almak ve size nasıl yardımcı olabileceğimizi anlatmak için bizimle görüşün.\n\n[Başarımı arttırmak hakkında ayrıntılı bilgi alın](https://www.mattermost.com/docs-elasticsearch/?utm_medium=product&utm_source=mattermost-advisor-bot&utm_content=elasticsearch)\n\nDenemeyi başlat üzerine tıklayarak [Mattermost yazılım deneme sözleşmesi](https://mattermost.com/software-evaluation-agreement/) ve [Kişisel verilerin gizliliği ilkesi](https://mattermost.com/pl/privacy-policy/) metinleri ile ürün tanıtımı e-postalarını almayı kabul ediyorum." }, { "id": "app.channel.save_member.exists.app_error", @@ -7289,7 +7285,7 @@ }, { "id": "ent.elasticsearch.indexer.do_job.get_oldest_entity.error", - "translation": "En eski varlık (kullanıcı, kanal ya da ileti) veritabanından alınamadı" + "translation": "Veri tabanından en eski varlık (kullanıcı, kanal ya da ileti) alınamadı" }, { "id": "app.user.verify_email.app_error", @@ -7385,7 +7381,7 @@ }, { "id": "app.user.get_known_users.get_users.app_error", - "translation": "Veritabanından bilinen kullanıcılar alınamadı." + "translation": "Veri tabanından bilinen kullanıcılar alınamadı." }, { "id": "app.user.get_by_username.app_error", @@ -7505,7 +7501,7 @@ }, { "id": "api.server.warn_metric.number_of_posts_2M.notification_body", - "translation": "Mattermost sisteminizde çok sayıda ileti var. Varsayılan Mattermost veritabanı aramasının başarımı 2,5 milyon iletiye yaklaşıldığında düşmeye başlar. 5 milyondan fazla iletide Elasticsearch kullanmak, arama ve anma durumlarında zaman aşımları gibi önemli başarım sorunlarını önleyebilir. Ayrıntılı bilgi almak ve size nasıl yardımcı olabileceğimizi anlatmak için bizimle görüşün.\n\n[Başarımı arttırmak hakkında ayrıntılı bilgi alın](https://www.mattermost.com/docs-elasticsearch/?utm_medium=product&utm_source=mattermost-advisor-bot&utm_content=elasticsearch)\n\nBizimle görüşün üzerine tıkladığınızda bilgileriniz Mattermost, Inc. tarafına gönderilir. [Ayrıntılı bilgi alın](https://mattermost.com/pl/default-admin-advisory)" + "translation": "Mattermost sisteminizde çok sayıda ileti var. Varsayılan Mattermost veri tabanı aramasının başarımı 2,5 milyon iletiye yaklaşıldığında düşmeye başlar. 5 milyondan fazla iletide Elasticsearch kullanmak, arama ve anma durumlarında zaman aşımları gibi önemli başarım sorunlarını önleyebilir. Ayrıntılı bilgi almak ve size nasıl yardımcı olabileceğimizi anlatmak için bizimle görüşün.\n\n[Başarımı arttırmak hakkında ayrıntılı bilgi alın](https://www.mattermost.com/docs-elasticsearch/?utm_medium=product&utm_source=mattermost-advisor-bot&utm_content=elasticsearch)\n\nBizimle görüşün üzerine tıkladığınızda bilgileriniz Mattermost, Inc. tarafına gönderilir. [Ayrıntılı bilgi alın](https://mattermost.com/pl/default-admin-advisory)" }, { "id": "api.server.warn_metric.number_of_active_users_500.start_trial.notification_body", @@ -7521,7 +7517,7 @@ }, { "id": "api.server.warn_metric.number_of_channels_50.notification_body", - "translation": "Kanallar iletişimi geliştirmeye yardımcı olur. Ancak Mattermost üzerindeki kullanıcıların kanallara katılması ve kanal oluşturması ile sistemi düzenli tutmak zorlaşır. Gelişmiş izinler, kanal ayarlarını ve üyeleri yönetmek, geniş kullanıcı gruplarını etiketlemek için @channel ya da @here kullanmak ve yeni web kancaları oluşturmak gibi işlemleri hangi kullanıcıların ya da rollerin yapabileceğini belirlemenizi sağlar.\n\n[Gelimiş izinleri kullanmak hakkında ayrıntılı bilgi alın](https://www.mattermost.com/docs-advanced-permissions/?utm_medium=product&utm_source=mattermost-advisor-bot&utm_content=advanced-permissions)\n\nBizimle görüşün üzerine tıkladığınızda bilgileriniz Mattermost, Inc. tarafına gönderilir. [Ayrıntılı bilgi alın](https://mattermost.com/pl/default-admin-advisory)" + "translation": "Kanallar iletişimi geliştirmeye yardımcı olur. Ancak Mattermost üzerindeki kullanıcıların kanallara katılması ve kanal oluşturması ile sistemi düzenli tutmak zorlaşır. Gelişmiş izinler, kanal ayarlarını ve üyeleri yönetmek, geniş kullanıcı gruplarını etiketlemek için @channel ya da @here kullanmak ve yeni internet kancaları oluşturmak gibi işlemleri hangi kullanıcıların ya da rollerin yapabileceğini belirlemenizi sağlar.\n\n[Gelişmiş izinleri kullanmak hakkında ayrıntılı bilgi alın](https://www.mattermost.com/docs-advanced-permissions/?utm_medium=product&utm_source=mattermost-advisor-bot&utm_content=advanced-permissions)\n\nBizimle görüşün üzerine tıkladığınızda bilgileriniz Mattermost, Inc. tarafına gönderilir. [Ayrıntılı bilgi alın](https://mattermost.com/pl/default-admin-advisory)" }, { "id": "api.server.warn_metric.number_of_active_users_300.start_trial.notification_body", @@ -7541,7 +7537,7 @@ }, { "id": "api.server.warn_metric.email_domain.start_trial.notification_body", - "translation": "Projelerde genellikle bir kuruluşun hem içinden hem de dışından kişiler bulunur. Konuk Hesapları ile dış iş ortaklarınız Mattermost sisteminize erişmesini sağlayarak, kiminle çalışabileceklerini ve neleri görebileceklerini belirleyebilirsiniz.\n\n[Konuk hesaplarını etkinleştirmek hakkında ayrıntılı bilgi alın](https://www.mattermost.com/docs-guest-accounts/?utm_medium=product&utm_source=mattermost-advisor-bot&utm_content=guest-accounts).\n\nDenemeyi başlat üzerine tıklayarak [Mattermost yazılım deneme sözleşmesi](https://mattermost.com/software-evaluation-agreement/) ve [Kişisel verilerin gizliliği ilkesi](https://mattermost.com/pl/privacy-policy/) metinleri ile ürün tanıtımı e-postalarını almayı kabul ediyorum." + "translation": "Projelerde genellikle bir kuruluşun hem içinden hem de dışından kişiler bulunur. Konuk hesapları ile dış iş ortaklarınız Mattermost sisteminize erişmesini sağlayarak, kiminle çalışabileceklerini ve neleri görebileceklerini belirleyebilirsiniz.\n\n[Konuk hesaplarını etkinleştirmek hakkında ayrıntılı bilgi alın](https://www.mattermost.com/docs-guest-accounts/?utm_medium=product&utm_source=mattermost-advisor-bot&utm_content=guest-accounts).\n\nDenemeyi başlat üzerine tıklayarak [Mattermost yazılım deneme sözleşmesi](https://mattermost.com/software-evaluation-agreement/) ve [Kişisel verilerin gizliliği ilkesi](https://mattermost.com/pl/privacy-policy/) metinleri ile ürün tanıtımı e-postalarını almayı kabul ediyorum." }, { "id": "api.server.warn_metric.number_of_active_users_300.start_trial.notification_success.message", @@ -8205,7 +8201,7 @@ }, { "id": "api.cloud.cws_webhook_event_missing_error", - "translation": "Web bağlantısı işlemi yapılamadı. Eksik ya da geçersiz olabilir." + "translation": "İnternet kancası işlemi yapılamadı. Eksik ya da geçersiz olabilir." }, { "id": "app.import.generate_password.app_error", @@ -8349,7 +8345,7 @@ }, { "id": "api.command_remote.site_url_not_set", - "translation": "Site adresi ayarlanmamış. Lütfen Sistem panosu > Ortam > Web sunucusu bölümünden ayarlayın." + "translation": "Site adresi ayarlanmamış. Lütfen Sistem panosu > Ortam > Site sunucusu bölümünden ayarlayın." }, { "id": "api.command_remote.service_not_enabled", @@ -8385,7 +8381,7 @@ }, { "id": "api.command_remote.remote_table_header", - "translation": "| Güvenli bağlantı | Görüntülenecek ad | Bağlantı kodu | Site adresi | Çağrı onaylandı | Çevrimiçi | Son iletişim |" + "translation": "| Güvenli bağlantı | Görüntülenecek ad | Bağlantı kodu | Site adresi | Çağrı onaylandı | Çevrim içi | Son iletişim |" }, { "id": "api.command_remote.permission_required", @@ -8453,7 +8449,7 @@ }, { "id": "sharedchannel.cannot_deliver_post", - "translation": "Çevrimdışı olduğundan, bir ya da daha fazla ileti {{.Remote}} uzak kümesine aktarılamadı. İletiler site yeniden çevrimiçi olduğunda aktarılacak." + "translation": "Çevrim dışı olduğundan, bir ya da daha fazla ileti {{.Remote}} uzak kümesine aktarılamadı. İletiler site yeniden çevrim içi olduğunda aktarılacak." }, { "id": "ent.data_retention.policies.invalid_policy", @@ -8565,7 +8561,7 @@ }, { "id": "api.command_share.remote_table_header", - "translation": "| Güvenli bağlantı | Site adresi | Salt okunur| Çağrı onaylandı | Çevrimiçi | Son eşitleme |" + "translation": "| Güvenli bağlantı | Site adresi | Salt okunur| Çağrı onaylandı | Çevrim içi | Son eşitleme |" }, { "id": "api.command_share.remote_id_invalid.error", @@ -8788,7 +8784,7 @@ }, { "id": "api.templates.license_up_for_renewal_subtitle", - "translation": "{{.UserName}}, aboneliğiniz {{.Days}} gün içinde sona erecek. Mattermost tarafından sunulan esnek ve güvenli ekip çalışmasını deneyimlediğinizi umuyoruz. Ekibinizin bu özellikleri kullanmayı sürdürebilmesi için aboneliğinizi yenileyin." + "translation": "{{.UserName}}, aboneliğiniz {{.Days}} gün içinde sona erecek. Mattermost tarafından sunulan esnek ve güvenli takım çalışmasını deneyimlediğinizi umuyoruz. Takımınızın bu özellikleri kullanmayı sürdürebilmesi için aboneliğinizi yenileyin." }, { "id": "api.templates.license_up_for_renewal_subject", @@ -9050,10 +9046,6 @@ "id": "model.config.is_valid.bleve_search.bulk_indexing_batch_size.app_error", "translation": "Bleve toplu dizine ekleme işlem boyutu en az {{.BatchSize}} olmalıdır." }, - { - "id": "api.insights.feature_disabled", - "translation": "İçgörtüler etkinleştirilmemiş bir özellik işaretinin arkasında." - }, { "id": "api.getThreadsForUser.bad_only_params", "translation": "getThreadsForUser için OnlyThreads ve OnlyTotals parametreleri aynı anda kullanılamaz" @@ -9122,10 +9114,6 @@ "id": "api.elasticsearch.test_elasticsearch_settings_nil.app_error", "translation": "Elasticsearch yapılandırmasında ayarlanmamış değerler var." }, - { - "id": "app.recent_searches.app_error", - "translation": "Son aramalar alınırken sorun çıktı" - }, { "id": "api.file.cloud_upload.app_error", "translation": "Bir Bulut kopyasına mmctl ile yükleme desteklenmiyor. Lütfen şu makaleye bakın: https://docs.mattermost.com/manage/cloud-data-export.html." @@ -9154,18 +9142,6 @@ "id": "model.config.is_valid.image_decoder_concurrency.app_error", "translation": "{{.Value}} kod çözücü eşzamanlılığı geçersiz. Pozitif bir sayı ya da -1 olmalıdır." }, - { - "id": "app.post.get_top_threads_for_user_since.app_error", - "translation": "Kullanıcının önde gelen konuları alınamadı." - }, - { - "id": "app.post.get_top_threads_for_team_since.app_error", - "translation": "Takımın önde gelen konuları alınamadı." - }, - { - "id": "app.insights.feature_disabled", - "translation": "İçgörü özelliği devre dışı bırakılmış." - }, { "id": "api.cloud.notify_admin_to_upgrade_error.already_notified", "translation": "Yönetici zaten bilgilendirilmiş" @@ -9198,10 +9174,6 @@ "id": "app.team.clear_cache.app_error", "translation": "Takım üyesi ön belleği temizlenirken sorun çıktı" }, - { - "id": "app.post.get_top_dms_for_user_since.app_error", - "translation": "Kullanıcının en fazla doğrudan iletileri alınamadı." - }, { "id": "api.command_help.success", "translation": "Mattermost, sağladığı araçlar ile takımların güvenli iletişim, işbirliği ve birlikte çalışmasını sağlayan açık kaynaklı bir platformdur.\nMattermost üç temel araç sunar:\n\n**Kanallar** - Takımınızla toplu olarak ve üyeleriyle birebir iletişim kurabilirsiniz.\n**[Senaryolar](/playbooks)** - Yinelenen işlemlere özel yapılandırmalar oluşturarak öngörülebilir sonuçlar alabilirsiniz.\n**[Panolar](/boards)** - Projeleri ve görevlerini Kanban panosu yapısında yöneterek takımınızın hedeflere ulaşmasına yardımcı olabilirsiniz.\n\n[İlgili belgeler ve rehberler]({{.HelpLink}})" @@ -9466,14 +9438,6 @@ "id": "api.team.invite_guests_to_channels.disabled.error", "translation": "Konuk hesapları devre dışı bırakılmış" }, - { - "id": "app.collection.add_topic.exists.app_error", - "translation": "Konu türü zaten var." - }, - { - "id": "app.collection.add_collection.exists.app_error", - "translation": "Derleme türü zaten var." - }, { "id": "api.drafts.disabled.app_error", "translation": "Taslaklar özelliği devre dışı." @@ -9498,10 +9462,6 @@ "id": "api.acknowledgement.delete.archived_channel.app_error", "translation": "Arşivlenmiş bir kanaldaki bir onayı kaldıramazsınız." }, - { - "id": "worktemplate.category.product_teams", - "translation": "Ürün" - }, { "id": "model.draft.is_valid.user_id.app_error", "translation": "Kullanıcı kodu geçersiz." @@ -9546,14 +9506,6 @@ "id": "model.acknowledgement.is_valid.post_id.app_error", "translation": "İleti kodu geçersiz." }, - { - "id": "app.worktemplates.get_templates.app_error", - "translation": "Çalışma kalıpları alınamadı" - }, - { - "id": "app.worktemplates.get_categories.app_error", - "translation": "Çalışma kalıbı kategorileri alınamadı" - }, { "id": "app.post_prority.get_for_post.app_error", "translation": "İletinin önceliği alınamadı" @@ -9656,15 +9608,15 @@ }, { "id": "api.server.cws.health_check.app_error", - "translation": "CWS sunucusu kullanılamıyor." + "translation": "Müşteri site sunucusu kullanılamıyor." }, { "id": "api.license.true_up_review.webhook_out_count_fail", - "translation": "Toplam giden web bağlantısı sayısı alınamadı" + "translation": "Toplam giden internet kancası sayısı alınamadı" }, { "id": "api.license.true_up_review.webhook_in_count_fail", - "translation": "Toplam gelen web bağlantısı sayısı alınamadı" + "translation": "Toplam gelen internet kancası sayısı alınamadı" }, { "id": "api.license.true_up_review.user_count_fail", @@ -9690,78 +9642,14 @@ "id": "api.templates.delinquency_45.subtitle2", "translation": "Alt tarifeye geçirilmiş bir çalışma alanında, yürütülen kritik iş akışları, bütünleştirmeler ve iş açısından önemli diğer işlemler olumsuz etkilenebilir." }, - { - "id": "app.worktemplate.execution_request.cannot_create_public_channel", - "translation": "Herkese açık bir kanal oluşturma izniniz yok." - }, - { - "id": "app.worktemplate.execution_request.cannot_create_public_board", - "translation": "Herkese açık bir pano oluşturma izniniz yok." - }, - { - "id": "app.worktemplate.execution_request.cannot_create_private_playbook", - "translation": "Özel bir senaryo oluşturma izniniz yok." - }, - { - "id": "app.worktemplate.execution_request.cannot_create_private_channel", - "translation": "Özel bir kanal oluşturma izniniz yok." - }, - { - "id": "app.worktemplate.execution_request.cannot_create_private_board", - "translation": "Özel bir pano oluşturma izniniz yok." - }, { "id": "app.group.username_conflict", "translation": "\"{{.Username}}\" adında bir kullanıcı zaten var." }, - { - "id": "api.work_templates.disabled", - "translation": "Çalışma kalıpları devre dışı bırakıldı." - }, - { - "id": "app.worktemplates.execute_work_template.playbooks.find_channel_error", - "translation": "Kanal ile ilişkilendirilmiş bir senaryo bulunamadı." - }, - { - "id": "app.worktemplates.execute_work_template.playbooks.create_error", - "translation": "Bir senaryo oluşturulurken sorun çıktı." - }, - { - "id": "app.worktemplates.execute_work_template.channels.create_error", - "translation": "Bir kanal oluşturulurken sorun çıktı." - }, - { - "id": "app.worktemplates.execute_work_template.boards.create_error", - "translation": "Bir pano oluşturulurken sorun çıktı." - }, - { - "id": "app.worktemplates.execute_work_template.app_error", - "translation": "Bir çalışma kalıbı yürütülürken sorun çıktı." - }, - { - "id": "app.worktemplate.execution_request.cannot_create_public_playbook", - "translation": "Herkese açık bir senaryo oluşturma izniniz yok." - }, - { - "id": "worktemplate.category.companywide", - "translation": "Kurum geneli" - }, - { - "id": "app.worktemplate.execution_request.license_cannot_create_private_playbook", - "translation": "Lisansınızda kişisel senaryo desteği yok." - }, { "id": "api.config.update_config.translations.app_error", "translation": "Sunucu çevirileri güncellenemedi." }, - { - "id": "worktemplate.category.leadership", - "translation": "Liderlik" - }, - { - "id": "worktemplate.category.devops", - "translation": "Geliştirme işlemleri" - }, { "id": "app.import.validate_user_teams_import_data.invalid_auth_service.error", "translation": "Kimlik doğrulama hizmeti geçersiz: {{.AuthService}}" @@ -9770,34 +9658,6 @@ "id": "api.server.cws.needs_enterprise_edition", "translation": "Bu hizmet yalnızca Mattermost Enterprise sürümüyle kullanılabilir" }, - { - "id": "app.worktemplates.execute_work_template.name_too_long", - "translation": "Ad alanına en fazla 64 karakter yazılabilir." - }, - { - "id": "app.team.create_onboarding_linked_board.product_key_not_found", - "translation": "ürün anahtarı bulunamadı" - }, - { - "id": "app.team.create_onboarding_linked_board.itf_not_of_type", - "translation": "arayüz beklenen türde değil" - }, - { - "id": "app.team.create_onboarding_linked_board.error_patching_board", - "translation": "pano düzenlenirken sorun çıktı" - }, - { - "id": "app.team.create_onboarding_linked_board.error_no_board", - "translation": "hata herhangi bir pano oluşturulmadı" - }, - { - "id": "app.team.create_onboarding_linked_board.error_getting_templates", - "translation": "pano kalıpları alınırken sorun çıktı" - }, - { - "id": "app.team.create_onboarding_linked_board.error_duplicating_board", - "translation": "pano kopyalanırken sorun çıktı" - }, { "id": "app.oauth.remove_auth_data_by_client_id.app_error", "translation": "Oauth verileri kaldırılamadı." @@ -9812,358 +9672,206 @@ }, { "id": "api.server.cws.delete_workspace.app_error", - "translation": "CWS sunucusu çalışma alanını silemedi." - }, - { - "id": "api.command_templates.unsupported.app_error", - "translation": "Kalıplar komutu aygıtınız için desteklenmiyor." - }, - { - "id": "api.command_templates.name", - "translation": "kalıplar" - }, - { - "id": "api.command_templates.desc", - "translation": "Kalıptan oluştur penceresini aç" - }, - { - "id": "worktemplate.product_teams.sprint_planning.board", - "translation": "Acil sorunlar, önceliklendirme, sahip atama ve yorumlarla ekibinizin haftalık hedeflere doğru ilerlemesini izleyin." - }, - { - "id": "worktemplate.product_teams.sprint_planning.integration", - "translation": "Zoom gibi sık kullandığınız araçlar ile bütünleştirerek kanalınızdaki üretkenliği artırın. Bunlar sizin için indirilir." - }, - { - "id": "worktemplate.product_teams.sprint_planning.channel", - "translation": "Panolarınız ve bütünleştirmelerinizle kolayca bağlantı kuran bir kanalda ekibinizle sohbet edin." - }, - { - "id": "worktemplate.product_teams.product_roadmap.channel", - "translation": "Müşterilerinizin geri bildirimleri ve önceliklendirme hakkında ekibinizle sohbet ederek birlikte ilerleme kaydedin." - }, - { - "id": "worktemplate.product_teams.product_roadmap.board", - "translation": "Kullanıcı geri bildirimlerini yönetmek, kaynak atamak, çıktıları takvimde görüntülemek ve sorunları önceliklendirmek için ürün yol haritası panosunu kullanın." - }, - { - "id": "worktemplate.product_teams.goals_and_okrs.integration", - "translation": "İşbirliğini kolaylaştırmak için Zoom gibi sık kullandığınız araçları bütünleştirin ve kanalınızdaki üretkenliği artırın. Bunlar sizin için indirilir." - }, - { - "id": "worktemplate.product_teams.goals_and_okrs.channel", - "translation": "Hedefleriniz ve ilerlemeniz hakkında ekibinizle farklı zamanlarda ya da gerçek zamanlı olarak sohbet edin ve değişiklikleri tek bir kanaldan izleyerek güncel kalın." - }, - { - "id": "worktemplate.product_teams.goals_and_okrs.board", - "translation": "Amaçlar ve anahtar sonuçlar (OKR) panosu ile ekibinizin kurumsal hedeflere doğru ilerlemesini izleyin. Toplantı gündemi panosu ile toplantıları izleyin." - }, - { - "id": "worktemplate.product_teams.feature_release.description.playbook", - "translation": "Özellik geliştirme sürecinizi destekleyen görev kontrol listeleri ve otomasyon ile işlevsel ekipler arasında işbirliğini artırın. İşiniz bittiğinde bir geçmiş değerlendirmesi yaparak ve süreci bir sonraki sürümünüz için iyileştirin." - }, - { - "id": "worktemplate.product_teams.feature_release.description.integration", - "translation": "Özellikleri yayınlamak için GitHub gibi sık kullandığınız araçları bütünleştirin ve kanalınızdaki üretkenliği artırın. Bunlar sizin için indirilir." - }, - { - "id": "worktemplate.product_teams.feature_release.description.channel", - "translation": "Panolarınıza, senaryolarınıza ve diğer bütünleştirmelere kolayca bağlanan bir kanalda sürüm engelleyicileri ve değişiklikler hakkında ekibinizle sohbet edin." - }, - { - "id": "worktemplate.product_teams.feature_release.description.board", - "translation": "Toplantı gündemi panosu ile toplantıları izleyin. Proje görevleri panosu ile iş yükünüzü yönetin." - }, - { - "id": "worktemplate.product_teams.bug_bash.playbook", - "translation": "Kapsamlı bir hata ayıklama süreci yürütmek için deneme alanları ve otomatik görevler atamak üzere kontrol listeleri kullanın. Sürecinizi gözden geçirmek ve gelecek sefer daha iyi olmasını sağlamak için bir geçmiş değerlendirmesi yapın." - }, - { - "id": "worktemplate.product_teams.bug_bash.integration", - "translation": "Hata ayıklama işlerinizi kolaylaştırmak için Jira gibi sık kullandığınız araçları bütünleştirin ve kanalınızdaki üretkenliği artırın. Bunlar sizin için indirilir." - }, - { - "id": "worktemplate.product_teams.bug_bash.channel", - "translation": "Hata raporlarını ve çözümlerini ekibinizin ve kuruluşunuzun kolayca erişebileceği tek bir kanalda planlayın ve yönetin." - }, - { - "id": "worktemplate.leadership.goals_and_okrs.integration", - "translation": "İşbirliğini kolaylaştırmak için Zoom gibi sık kullandığınız araçları bütünleştirin ve kanalınızdaki üretkenliği artırın. Bunlar sizin için indirilir." - }, - { - "id": "worktemplate.leadership.goals_and_okrs.channel", - "translation": "Hedefleriniz ve ilerlemeniz hakkında ekibinizle farklı zamanlarda ya da gerçek zamanlı olarak sohbet edin ve değişiklikleri tek bir kanaldan izleyerek güncel kalın." - }, - { - "id": "worktemplate.leadership.goals_and_okrs.board", - "translation": "Amaçlar ve anahtar sonuçlar (OKR) panosu ile ekibinizin kurumsal hedeflere doğru ilerlemesini izleyin. Toplantı gündemi panosu ile toplantıları izleyin." - }, - { - "id": "worktemplate.devops.product_release.playbook", - "translation": "Ürün çıkışlarının güvenilir ve zamanında olması için izlenmesi ve uygulanması kolay, yinelenebilir iş akışları oluşturun." - }, - { - "id": "worktemplate.devops.product_release.channel", - "translation": "Ekibinizle günlük kilometre taşları, engeller ve çıktılardaki değişiklikler hakkında kolay ve hızlı bir şekilde sohbet edin." - }, - { - "id": "worktemplate.devops.product_release.board", - "translation": "Sürüm zaman çerçevenizi ve sürecinizi desteklemek için ürün çıkarma panosunu kullanın ve herkesin hangi görevlerin bitiş zamanının geldiğini bilmesini sağlayın." - }, - { - "id": "worktemplate.devops.incident_resolution.description.playbook", - "translation": "Kilit ekip üyelerini bir araya getirmek için kontrol listeleri ve otomasyon kullanarak olayın çözüme doğru ilerlemesini paylaşın." - }, - { - "id": "worktemplate.devops.incident_resolution.description.channel", - "translation": "Ekibinizle tek bir kanal kullanarak öncelikler hakkında sohbet edin, paydaşlar ekleyin, güncellemeler yayınlayın ve çözüm için çalışın." - }, - { - "id": "worktemplate.devops.incident_resolution.description.board", - "translation": "Yinelenebilen süreçleri desteklemek ve ekip genelinde tanımlanmış görevler atamak için olay çözümleme panosunu kullanın." - }, - { - "id": "worktemplate.companywide.goals_and_okrs.integration", - "translation": "İşbirliğini kolaylaştırmak için Zoom gibi sık kullandığınız araçları bütünleştirin ve kanalınızdaki üretkenliği artırın. Bunlar sizin için indirilir." - }, - { - "id": "worktemplate.companywide.goals_and_okrs.channel", - "translation": "Hedefleriniz ve ilerlemeniz hakkında ekibinizle farklı zamanlarda ya da gerçek zamanlı olarak sohbet edin ve değişiklikleri tek bir kanaldan izleyerek güncel kalın." - }, - { - "id": "worktemplate.companywide.goals_and_okrs.board", - "translation": "Amaçlar ve anahtar sonuçlar (OKR) panosu ile ekibinizin kurumsal hedeflere doğru ilerlemesini izleyin. Toplantı gündemi panosu ile toplantıları izleyin." + "translation": "Müşteri site sunucusu çalışma alanını silemedi." }, { "id": "model.license_record.is_valid.bytes.app_error", "translation": "Bir lisans yüklenirken bayt değeri geçersiz." }, - { - "id": "worktemplate.companywide.create_project.integration", - "translation": "Sık kullandığınız araçlar ile bütünleştirerek kanalınızdaki üretkenliği artırın. Bunlar sizin için indirilir." - }, - { - "id": "worktemplate.companywide.create_project.channel", - "translation": "Ekibinizle yeni projeniz üzerine sohbet edin ve işbirlikli bir kanalda projeyi nasıl yapılandıracağınıza karar verin." - }, - { - "id": "worktemplate.companywide.create_project.board", - "translation": "Proje görevlerinizi ve ilerlemenizi tanımlamak ve izlemek için bir Kanban panosu kullanın." - }, - { - "id": "app.user.run.update_status.title", - "translation": "Durum güncellemesi" - }, - { - "id": "app.user.run.update_status.submit_label", - "translation": "Durumu güncelle" - }, - { - "id": "app.user.run.update_status.reminder_for_next_update", - "translation": "Sonraki güncelleme anımsatıcısı" - }, - { - "id": "app.user.run.update_status.num_channel", - "translation": { - "one": "Paydaşlara bir güncelleme duyurun. Bu gönderi {{.Count}} kanalında yayınlanacak.", - "other": "Paydaşlara bir güncelleme duyurun. Bu gönderi {{.Count}} kanalında yayınlanacak." - } - }, - { - "id": "app.user.run.update_status.finish_run.placeholder", - "translation": "Ayrıca oyunu da tamamlanmış olarak işaretle" - }, - { - "id": "app.user.run.update_status.finish_run", - "translation": "Oyunu tamamla" - }, - { - "id": "app.user.run.update_status.change_since_last_update", - "translation": "Son güncellemeden sonraki değişiklik" - }, - { - "id": "app.user.run.status_enable", - "translation": "@{{.Username}}, [{{.RunName}}]({{.RunURL}}) için durum güncellemelerini etkinleştirdi" - }, - { - "id": "app.user.run.status_disable", - "translation": "@{{.Username}}, [{{.RunName}}]({{.RunURL}}) için durum güncellemelerini devre dışı bıraktı" - }, - { - "id": "app.user.run.request_update", - "translation": "@here — @{{.Name}}, [{{.RunName}}]({{.RunURL}}) için bir durum güncellemesi istedi. \n" - }, - { - "id": "app.user.run.request_join_channel", - "translation": "@{{.Name}} bir oyun katılımcısı ve bu kanala katılmak istiyor. Kanalın herhangi bir üyesi onu çağırabilir.\n" - }, - { - "id": "app.user.run.confirm_finish.title", - "translation": "Oyunu tamamlamayı onayla" - }, - { - "id": "app.user.run.confirm_finish.submit_label", - "translation": "Oyunu tamamla" - }, - { - "id": "app.user.run.confirm_finish.num_outstanding", - "translation": { - "one": "Bekleyen **{{.Count}} görev** var. *{{.RunName}}* oyununu tüm katılımcılar için tamamlamak istediğinize emin misiniz?", - "other": "Bekleyen **{{.Count}} görev** var. *{{.RunName}}* oyununu tüm katılımcılar için tamamlamak istediğinize emin misiniz?" - } - }, - { - "id": "app.user.run.add_to_timeline.title", - "translation": "Oyun zaman akışına ekle" - }, - { - "id": "app.user.run.add_to_timeline.summary.placeholder", - "translation": "Zaman akışında görüntülenecek kısa açıklama" - }, - { - "id": "app.user.run.add_to_timeline.summary.help", - "translation": "En fazla 64 karakter" - }, - { - "id": "app.user.run.add_to_timeline.summary", - "translation": "Özet" - }, - { - "id": "app.user.run.add_to_timeline.submit_label", - "translation": "Oyun zaman akışına ekle" - }, - { - "id": "app.user.run.add_to_timeline.playbook_run", - "translation": "Senaryo oyunu" - }, - { - "id": "app.user.run.add_checklist_item.title", - "translation": "Yeni görev ekle" - }, - { - "id": "app.user.run.add_checklist_item.submit_label", - "translation": "Görev ekle" - }, - { - "id": "app.user.run.add_checklist_item.name", - "translation": "Ad" - }, - { - "id": "app.user.run.add_checklist_item.description", - "translation": "Açıklama" - }, - { - "id": "app.user.new_run.title", - "translation": "Senaryoyu oyna" - }, - { - "id": "app.user.new_run.submit_label", - "translation": "Oyunu başlat" - }, - { - "id": "app.user.new_run.run_name", - "translation": "Oyun adı" - }, - { - "id": "app.user.new_run.playbook", - "translation": "Senaryo" - }, { "id": "app.user.new_run.intro", "translation": "**Sahibi** {{.Username}}" }, - { - "id": "app.user.digest.tasks.zero_assigned", - "translation": "Size atanmış bir görev yok." - }, - { - "id": "app.user.digest.tasks.num_assigned_due_until_today", - "translation": { - "one": "Süresi dolmuş {{.Count}} atanmış göreviniz var:", - "other": "Süresi dolmuş {{.Count}} atanmış göreviniz var:" - } - }, - { - "id": "app.user.digest.tasks.num_assigned", - "translation": { - "one": "{{.Count}} atanmış göreviniz var:", - "other": "{{.Count}} atanmış göreviniz var:" - } - }, - { - "id": "app.user.digest.tasks.heading", - "translation": "Atanmış görevleriniz" - }, - { - "id": "app.user.digest.tasks.due_yesterday", - "translation": "Süresi dün doldu" - }, - { - "id": "app.user.digest.tasks.due_x_days_ago", - "translation": "Süresi {{.Count}} gün önce doldu" - }, - { - "id": "app.user.digest.tasks.due_today", - "translation": "Süresi bugün dolacak" - }, - { - "id": "app.user.digest.tasks.due_in_x_days", - "translation": { - "one": "{{.Count}} gün içinde süresi dolacak", - "other": "{{.Count}} gün içinde süresi dolacak" - } - }, - { - "id": "app.user.digest.tasks.due_after_today", - "translation": { - "one": "Bugünden sonra süresi dolacak **{{.Count}} göreviniz var**.", - "other": "Bugünden sonra süresi dolacak **{{.Count}} göreviniz var**." - } - }, - { - "id": "app.user.digest.tasks.all_tasks_command", - "translation": "Tüm görevlerinizi görüntülemek için `/playbook todo` kullanın." - }, - { - "id": "app.user.digest.runs_in_progress.zero_in_progress", - "translation": "Süren bir oyununuz yok." - }, - { - "id": "app.user.digest.runs_in_progress.num_in_progress", - "translation": { - "one": "Süren {{.Count}} oyununuz var:", - "other": "Süren {{.Count}} oyununuz var:" - } - }, - { - "id": "app.user.digest.runs_in_progress.heading", - "translation": "Süren oyunlar" - }, - { - "id": "app.user.digest.overdue_status_updates.zero_overdue", - "translation": "Gecikmiş bir oyununuz yok." - }, - { - "id": "app.user.digest.overdue_status_updates.num_overdue", - "translation": { - "one": "Bir durum güncellemesi için {{.Count}} oyun gecikmeniz var:", - "other": "Bir durum güncellemesi için {{.Count}} oyun gecikmeniz var:" - } - }, - { - "id": "app.user.digest.overdue_status_updates.heading", - "translation": "Gecikmiş durum güncellemeleri" - }, - { - "id": "app.command.execute.error", - "translation": "Komut yürütülemedi." - }, { "id": "api.server.cws.subscribe_to_newsletter.app_error", - "translation": "CWS sunucusu duyurulara abone olamadı." + "translation": "Müşteri site sunucusu duyurulara abone olamadı." }, { "id": "api.license.request-trial.bad-request.business-email", "translation": "Deneme için iş e-postası geçersiz" + }, + { + "id": "model.config.is_valid.persistent_notifications_recipients.app_error", + "translation": "Kalıcı bildirimler için en fazla alıcı sayısı geçersiz. Pozitif bir sayı olmalıdır." + }, + { + "id": "model.config.is_valid.elastic_search.ignored_indexes_dash_prefix.app_error", + "translation": "Silinmek için yok sayılan dizinler tire ile başlamamalıdır." + }, + { + "id": "app.save_config.plugin_hook_error", + "translation": "Yapılandırma kaydında uygulama eki kancası yürütülürken bir sorun çıktı." + }, + { + "id": "api.post.post_priority.max_recipients_persistent_notification_post.request_error", + "translation": "Kalıcı bildirim gönderiminde en fazla {{.MaxRecipients}} alıcı olabilir." + }, + { + "id": "api.license.true_up_review.failed_to_submit", + "translation": "Lisans sayısı değerlendirme profili müşteri site sunucusuna gönderilemedi." + }, + { + "id": "api.post.post_priority.priority_post_only_allowed_for_root_post.request_error", + "translation": "Yalnızca root gönderilerinin önceliği olmasına izin veriliyor." + }, + { + "id": "api.user.invalidate_password_recovery_tokens.error", + "translation": "Parola kurtarma belirteçleri geçersiz kılınırken belirteçler türe göre alınamadı" + }, + { + "id": "api.user.invalidate_password_recovery_tokens_delete.error", + "translation": "Parola kurtarma belirteçleri geçersiz kılınırken belirteçler kaldırılamadı" + }, + { + "id": "app.post_persistent_notification.delete_by_channel.app_error", + "translation": "Kanala göre kalıcı bildirimler silinemedi." + }, + { + "id": "app.post_priority.delete_persistent_notification_post.app_error", + "translation": "Kalıcı bildirim iletisi silinemedi" + }, + { + "id": "app.plugin.skip_installation.app_error", + "translation": "Kurulu sürümü aynı ya da daha yeni olduğundan {{.Id}} uygulama ekinin kurulumu atlanıyor." + }, + { + "id": "model.config.is_valid.local_mode_socket.app_error", + "translation": "Yerel soket dosyası klasörü bulunamadı." + }, + { + "id": "model.config.is_valid.persistent_notifications_count.app_error", + "translation": "Gönderi başına toplam kalıcı bildirim sayısı geçersiz. Pozitif bir sayı olmalıdır." + }, + { + "id": "model.config.is_valid.persistent_notifications_interval.app_error", + "translation": "Kalıcı bildirimlerin geçersiz sayılacağı sıklık. En az iki dakika olmalıdır." + }, + { + "id": "api.error_no_organization_name_provided_for_self_hosted_onboarding", + "translation": "Hata: Özel barındırılan kullanıma geçmek için kuruluş adı belirtilmemiş." + }, + { + "id": "api.post.post_priority.min_recipients_persistent_notification_post.request_error", + "translation": "Kalıcı bildirim gönderiminde en az 1 alıcı olmalıdır." + }, + { + "id": "app.file_info.get.gif.app_error", + "translation": "Gif kodu çözülemedi." + }, + { + "id": "api.command_exportlink.desc", + "translation": "Bir dışa aktarmayı indirmek için bir bağlantı oluşturur." + }, + { + "id": "api.command_exportlink.driver.app_error", + "translation": "Dosya kaydetme sürücüsü bağlantı oluşturulmasını desteklemiyor." + }, + { + "id": "api.command_exportlink.empty.app_error", + "translation": "Bir dışa aktarma dosyası bulunamadı." + }, + { + "id": "api.command_exportlink.hint", + "translation": "[job-id|zip dosya adı|{{.LatestMsg}}]" + }, + { + "id": "api.command_exportlink.invalid.app_error", + "translation": "İstediğiniz dosya bulunamadı." + }, + { + "id": "api.command_exportlink.link.text", + "translation": "Dosyanızı şuradan indirebilirsiniz: {{.Link}}.\nBu bağlantı {{.Expiration}} sonra geçersiz olacak." + }, + { + "id": "api.command_exportlink.list.app_error", + "translation": "Dışa aktarma listesi alınamadı." + }, + { + "id": "api.command_exportlink.name", + "translation": "dışaaktarmabağlantısı" + }, + { + "id": "api.command_exportlink.permission.app_error", + "translation": "Bu komutu yürütme izniniz yok." + }, + { + "id": "api.command_exportlink.presign.app_error", + "translation": "Önceden imzalanmış adres oluşturulamadı." + }, + { + "id": "app.channel.get_channels_member_count.existing.app_error", + "translation": "Belirtilen kanalların üye sayısı bulunamadı." + }, + { + "id": "app.channel.get_channels_member_count.find.app_error", + "translation": "Üye sayısı bulunamadı." + }, + { + "id": "app.channel.update_member.app_error", + "translation": "Kanal üyesi güncellenemedi." + }, + { + "id": "app.channel.update_member.notify_props_limit_exceeded.app_error", + "translation": "Kanal üyesi güncellenemedi. Bildirim özellikleri boyutu sınırına ulaşıldı." + }, + { + "id": "app.eport.generate_presigned_url.config.app_error", + "translation": "Bu işlemler için ayrılmış bir dışa aktarma depolaması kullanılmalıdır." + }, + { + "id": "app.eport.generate_presigned_url.driver.app_error", + "translation": "Dışa aktarma depolaması sürücünüz önceden imzalanmış adreslerin oluşturulmasını desteklemiyor." + }, + { + "id": "app.eport.generate_presigned_url.featureflag.app_error", + "translation": "Bu özellik bir özellik işareti ile kısıtlanmış." + }, + { + "id": "app.eport.generate_presigned_url.fileexist.app_error", + "translation": "Dosyanın var olup olmadığı denetlenemedi." + }, + { + "id": "app.eport.generate_presigned_url.link.app_error", + "translation": "Önceden imzalanmış adres oluşturulamadı." + }, + { + "id": "app.eport.generate_presigned_url.notfound.app_error", + "translation": "Dışa aktarma dosyası bulunamadı." + }, + { + "id": "app.post_persistent_notification.delete_by_team.app_error", + "translation": "Takıma göre kalıcı bildirimler silinemedi." + }, + { + "id": "ent.elasticsearch.max_version.app_error", + "translation": "Elasticsearch {{.Version}} sürümü, desteklenen {{.MaxVersion}} sürümünden daha yeni" + }, + { + "id": "model.channel_member.is_valid.channel_auto_follow_threads_value.app_error", + "translation": "channel-auto-follow-threads değeri geçersiz." + }, + { + "id": "model.channel_member.is_valid.notify_props.app_error", + "translation": "Bildirim özellikleri boyutu sınırına ulaşıldı." + }, + { + "id": "app.post.delete_post.get_team.app_error", + "translation": "Takım alınırken bir sorun çıktı." + }, + { + "id": "api.post.post_priority.priority_post_not_allowed_for_user.request_error", + "translation": "Kullanıcının öncelikli gönderim ya da kalıcı bildirim oluşturma izni yok." + }, + { + "id": "api.post.post_priority.urgent_persistent_notification_post.request_error", + "translation": "Kalıcı bildirim gönderilerinin önceliği Acil olmalıdır." + }, + { + "id": "api.user.invalidate_password_recovery_tokens_parse.error", + "translation": "Parola kurtarma belirteçleri geçersiz kılınırken belirteç işlenemedi" + }, + { + "id": "api.post.post_priority.persistent_notification_validation_error.request_error", + "translation": "Kalıcı bildirim doğrulanamadı." } ] diff --git a/server/i18n/uk.json b/server/i18n/uk.json index de5501e1a63..4ad78c5b998 100644 --- a/server/i18n/uk.json +++ b/server/i18n/uk.json @@ -4043,10 +4043,6 @@ "id": "model.emoji.user_id.app_error", "translation": "Неприпустимий ідентифікатор автора" }, - { - "id": "model.file_info.get.gif.app_error", - "translation": "Чи не вдалося декодувати gif." - }, { "id": "model.file_info.is_valid.create_at.app_error", "translation": "Неприпустиме значення для часу очікування читання." diff --git a/server/i18n/zh-CN.json b/server/i18n/zh-CN.json index b0f6ed6109d..4e87daef07e 100644 --- a/server/i18n/zh-CN.json +++ b/server/i18n/zh-CN.json @@ -1785,7 +1785,7 @@ }, { "id": "api.templates.email_info1", - "translation": "如有任何问题,请随时给我们发邮件:" + "translation": "如有任何问题,请随时给我们发邮件: " }, { "id": "api.templates.email_info2", @@ -3297,7 +3297,7 @@ }, { "id": "ent.migration.migratetoldap.user_not_found", - "translation": "未在 AD/LDAP 服务器上找到用户:" + "translation": "未在 AD/LDAP 服务器上找到用户: " }, { "id": "ent.migration.migratetosaml.email_already_used_by_other_user", @@ -4035,10 +4035,6 @@ "id": "model.emoji.user_id.app_error", "translation": "无效创建者 id。" }, - { - "id": "model.file_info.get.gif.app_error", - "translation": "无法解码 gif。" - }, { "id": "model.file_info.is_valid.create_at.app_error", "translation": "错误的 create_at 值。" @@ -5825,7 +5821,7 @@ }, { "id": "web.error.unsupported_browser.min_browser_version.edge", - "translation": "版本 110+" + "translation": "版本 112+" }, { "id": "web.error.unsupported_browser.min_browser_version.chrome", @@ -9043,10 +9039,6 @@ "id": "api.license.request_trial_license.embargoed", "translation": "由于对禁运国家的限制,我们无法处理请求。[在我们的文档中了解更多信息](https://mattermost.com/pl/limitations-for-embargoed-countries),或联系legal@mattermost.com了解有关出口限制的问题。" }, - { - "id": "api.insights.feature_disabled", - "translation": "Insights 位于未启用的特性标志后面。" - }, { "id": "api.getThreadsForUser.bad_only_params", "translation": "参数 getThreadsForUser 的 OnlyThreads 和 OnlyTotals 是相互排斥的(互诉的)" @@ -9111,18 +9103,10 @@ "id": "app.user.update_thread_read_for_user_by_post.app_error", "translation": "无效的 post_id" }, - { - "id": "app.recent_searches.app_error", - "translation": "获取最近搜索错误" - }, { "id": "app.post.analytics_teams_count.app_error", "translation": "无法获取团队使用情况" }, - { - "id": "app.insights.feature_disabled", - "translation": "Insights 功能已禁用。" - }, { "id": "api.file.cloud_upload.app_error", "translation": "不支持通过 mmctl 上传到 Cloud 实例。 请在此处查看文档:https://docs.mattermost.com/manage/cloud-data-export.html。" @@ -9175,18 +9159,6 @@ "id": "app.team.clear_cache.app_error", "translation": "清除团队成员缓存时出错" }, - { - "id": "app.post.get_top_threads_for_user_since.app_error", - "translation": "无法为用户获取顶级线程。" - }, - { - "id": "app.post.get_top_threads_for_team_since.app_error", - "translation": "无法为团队获取顶级线程。" - }, - { - "id": "app.post.get_top_dms_for_user_since.app_error", - "translation": "无法为用户获取顶级 DMs。" - }, { "id": "app.post.cloud.get.app_error", "translation": "无法获取帖子,因为它超过了云的计划限制。" @@ -9439,34 +9411,10 @@ "id": "api.templates.delinquency_75.subtitle1", "translation": "这是自 {{.DelinquencyDate}} 后我们尚未收到您的 Mattermost Cloud 工作区付款的最后提醒。" }, - { - "id": "api.command_templates.name", - "translation": "模板" - }, - { - "id": "api.command_templates.desc", - "translation": "打开从模板创建窗口" - }, { "id": "api.templates.delinquency_30.subtitle1", "translation": "您还有时间保持您的 Mattermost {{.Plan}} 可用,但是在那之前您需要解决一些与支付有关的问题。" }, - { - "id": "worktemplate.category.product_teams", - "translation": "产品" - }, - { - "id": "worktemplate.category.leadership", - "translation": "领导" - }, - { - "id": "worktemplate.category.devops", - "translation": "DevOps" - }, - { - "id": "worktemplate.category.companywide", - "translation": "全公司" - }, { "id": "model.group.name.reserved_name.app_error", "translation": "群组名称已作为保留名称存在" @@ -9539,94 +9487,10 @@ "id": "ent.elasticsearch.create_client.ca_cert_malformed", "translation": "解码 Elasticsearch CA 失败" }, - { - "id": "app.worktemplates.get_templates.app_error", - "translation": "无法取得作业模板" - }, - { - "id": "app.worktemplates.get_categories.app_error", - "translation": "无法取得作业模板目录" - }, - { - "id": "app.worktemplates.execute_work_template.playbooks.find_channel_error", - "translation": "找不到与 Playbook 关联的频道。" - }, - { - "id": "app.worktemplates.execute_work_template.playbooks.create_error", - "translation": "创建 Playbook 时发生错误。" - }, - { - "id": "app.worktemplates.execute_work_template.name_too_long", - "translation": "名称栏目中无法输入超过 64 个字符的内容。" - }, - { - "id": "app.worktemplates.execute_work_template.channels.create_error", - "translation": "在创建频道时发生错误。" - }, - { - "id": "app.worktemplates.execute_work_template.boards.create_error", - "translation": "在创建 Board 时发生错误。" - }, - { - "id": "app.worktemplates.execute_work_template.app_error", - "translation": "在执行作业模板时发生错误。" - }, - { - "id": "app.worktemplate.execution_request.license_cannot_create_private_playbook", - "translation": "您的授权不支持私密 Playbook。" - }, - { - "id": "app.worktemplate.execution_request.cannot_create_public_playbook", - "translation": "您没有创建公开 Playbook 的权限。" - }, - { - "id": "app.worktemplate.execution_request.cannot_create_public_channel", - "translation": "您没有创建公开频道的权限。" - }, - { - "id": "app.worktemplate.execution_request.cannot_create_public_board", - "translation": "您没有创建公开 Board 的权限。" - }, - { - "id": "app.worktemplate.execution_request.cannot_create_private_playbook", - "translation": "您没有创建私密 Playbook 的权限。" - }, - { - "id": "app.worktemplate.execution_request.cannot_create_private_channel", - "translation": "您没有创建私密频道的权限。" - }, - { - "id": "app.worktemplate.execution_request.cannot_create_private_board", - "translation": "您没有创建私密 Board 的权限。" - }, { "id": "app.user.get_badge_count.app_error", "translation": "无法获取用户的奖章数目。" }, - { - "id": "app.team.create_onboarding_linked_board.product_key_not_found", - "translation": "未找到产品密钥" - }, - { - "id": "app.team.create_onboarding_linked_board.itf_not_of_type", - "translation": "接口不是预期的类型" - }, - { - "id": "app.team.create_onboarding_linked_board.error_patching_board", - "translation": "更新 Board 时发生错误" - }, - { - "id": "app.team.create_onboarding_linked_board.error_no_board", - "translation": "在创建 Board 时发生错误" - }, - { - "id": "app.team.create_onboarding_linked_board.error_getting_templates", - "translation": "在取得 Board 模板时发生错误" - }, - { - "id": "app.team.create_onboarding_linked_board.error_duplicating_board", - "translation": "在复制 Board 时发生错误" - }, { "id": "app.post_prority.get_for_post.app_error", "translation": "无法取得消息的优先级" @@ -9683,14 +9547,6 @@ "id": "app.draft.delete.app_error", "translation": "无法删除草稿。" }, - { - "id": "app.collection.add_topic.exists.app_error", - "translation": "Topic 类型已存在。" - }, - { - "id": "app.collection.add_collection.exists.app_error", - "translation": "Collection 类型已存在。" - }, { "id": "app.cloud.upgrade_plan_bot_message_single", "translation": "来自工作区 {{.WorkspaceName}} 的 {{.UsersNum}} 位成员已申请工作区的升级: " @@ -9747,10 +9603,6 @@ "id": "app.acknowledgement.delete.app_error", "translation": "无法删除已读确认。" }, - { - "id": "api.work_templates.disabled", - "translation": "工作模板已被禁用。" - }, { "id": "api.user.get_users.validation.app_error", "translation": "在获取用户权限的过程中发生错误。" @@ -9791,214 +9643,16 @@ "id": "api.server.cws.delete_workspace.app_error", "translation": "CWS 服务器删除工作区时失败。" }, - { - "id": "api.command_templates.unsupported.app_error", - "translation": "您的设备不支持模板命令。" - }, - { - "id": "app.user.run.update_status.title", - "translation": "状态更新" - }, - { - "id": "app.user.run.update_status.submit_label", - "translation": "更新状态" - }, - { - "id": "app.user.run.update_status.reminder_for_next_update", - "translation": "下次更新的提醒" - }, - { - "id": "app.user.run.update_status.num_channel", - "translation": { - "other": "为利益相关者提供一次更新提醒。这条提醒将被广播到{{.Count}} 个频道。" - } - }, - { - "id": "app.user.run.update_status.finish_run.placeholder", - "translation": "并且标记此运行为已结束" - }, - { - "id": "app.user.run.update_status.finish_run", - "translation": "结束运行" - }, - { - "id": "app.user.run.update_status.change_since_last_update", - "translation": "对比上次存在更改" - }, - { - "id": "app.user.run.status_enable", - "translation": "@{{.Username}} 启用了对 [{{.RunName}}]({{.RunURL}})的状态更新" - }, - { - "id": "app.user.run.status_disable", - "translation": "@{{.Username}} 停止用了 [{{.RunName}}]({{.RunURL}})的状态更新。" - }, - { - "id": "app.user.run.request_update", - "translation": "@here — @{{.Name}} 请求对 [{{.RunName}}]({{.RunURL}}) 进行状态更新。 \n" - }, - { - "id": "app.user.run.request_join_channel", - "translation": "@{{.Name}} 是一个运行的参与者,并且希望要参加这个频道。任何的频道成员都可以邀请他们。\n" - }, - { - "id": "app.user.run.confirm_finish.title", - "translation": "确认完成运行" - }, - { - "id": "app.user.run.confirm_finish.submit_label", - "translation": "完成运行" - }, - { - "id": "app.user.run.confirm_finish.num_outstanding", - "translation": { - "other": "一共有 **{{.Count}} 未完成的任务**. 您确定想为所有的参与者结束*{{.RunName}}*吗?" - } - }, - { - "id": "app.user.run.add_to_timeline.title", - "translation": "添加到运行队列" - }, - { - "id": "app.user.run.add_to_timeline.summary.placeholder", - "translation": "时间表里显示的简单概要" - }, - { - "id": "app.user.run.add_to_timeline.summary.help", - "translation": "最大64个字符" - }, - { - "id": "app.user.run.add_to_timeline.summary", - "translation": "概要" - }, - { - "id": "app.user.run.add_to_timeline.submit_label", - "translation": "添加到运行队列" - }, - { - "id": "app.user.run.add_to_timeline.playbook_run", - "translation": "Playbook运行" - }, - { - "id": "app.user.run.add_checklist_item.title", - "translation": "添加新任务" - }, - { - "id": "app.user.run.add_checklist_item.submit_label", - "translation": "添加任务" - }, - { - "id": "app.user.run.add_checklist_item.name", - "translation": "名字" - }, - { - "id": "app.user.run.add_checklist_item.description", - "translation": "描述" - }, - { - "id": "app.user.new_run.title", - "translation": "运行playbook" - }, - { - "id": "app.user.new_run.submit_label", - "translation": "开始运行" - }, - { - "id": "app.user.new_run.run_name", - "translation": "运行名" - }, - { - "id": "app.user.new_run.playbook", - "translation": "Playbook" - }, - { - "id": "app.user.new_run.intro", - "translation": "**所有者** {{.Username}}" - }, - { - "id": "app.user.digest.tasks.zero_assigned", - "translation": "您没有任务。" - }, { "id": "app.user.digest.tasks.num_assigned_due_until_today", "translation": { "other": "您有 {{.Count}} 个任务现在已经逾期:" } }, - { - "id": "app.user.digest.tasks.num_assigned", - "translation": { - "other": "您一共有{{.Count}}个任务:" - } - }, - { - "id": "app.user.digest.tasks.heading", - "translation": "您的任务" - }, - { - "id": "app.user.digest.tasks.due_yesterday", - "translation": "于昨天过期" - }, - { - "id": "app.user.digest.tasks.due_x_days_ago", - "translation": "{{.Count}}天已过期" - }, - { - "id": "app.user.digest.tasks.due_today", - "translation": "将于今天过期" - }, - { - "id": "app.user.digest.tasks.due_in_x_days", - "translation": { - "other": "{{.Count}}天后过期" - } - }, - { - "id": "app.user.digest.tasks.due_after_today", - "translation": { - "other": "您有 **{{.Count}} 项目任务今天之后将要过期**." - } - }, - { - "id": "app.user.digest.tasks.all_tasks_command", - "translation": "请使用`/playbook todo`来查看您所有的任务。" - }, - { - "id": "app.user.digest.runs_in_progress.zero_in_progress", - "translation": "您有0项正在运行。" - }, - { - "id": "app.user.digest.runs_in_progress.num_in_progress", - "translation": { - "other": "您有{{.Count}}正在运行:" - } - }, - { - "id": "app.user.digest.runs_in_progress.heading", - "translation": "正在运行" - }, - { - "id": "app.user.digest.overdue_status_updates.zero_overdue", - "translation": "您没有逾期。" - }, - { - "id": "app.user.digest.overdue_status_updates.num_overdue", - "translation": { - "other": "您有 {{.Count}} 过期需要状态更新:" - } - }, - { - "id": "app.user.digest.overdue_status_updates.heading", - "translation": "过期状态更新" - }, { "id": "app.oauth.remove_auth_data_by_client_id.app_error", "translation": "不能删除oauth认证信息。" }, - { - "id": "app.command.execute.error", - "translation": "无法执行命令。" - }, { "id": "api.templates.license_up_for_renewal_contact_sales", "translation": "联系销售" @@ -10024,63 +9678,195 @@ "translation": "无效的商务试用邮箱" }, { - "id": "worktemplate.product_teams.feature_release.description.integration", - "translation": "通过集成大多您使用过的工具(比如 GitHub)在您的频道里提高生产力,实现你的功能发布。这些工具将为你下载。" + "id": "api.error_no_organization_name_provided_for_self_hosted_onboarding", + "translation": "未提供自助托管引导的组织名称。" }, { - "id": "worktemplate.product_teams.feature_release.description.channel", - "translation": "在一个频道与你的团队讨论任何的发布障碍和变动,并很容易的与你的面板,Playbook或者其他的集成功能连接。" + "id": "api.license.true_up_review.failed_to_submit", + "translation": "向 CWS 提交 true up 评审配置文件失败。" }, { - "id": "worktemplate.product_teams.feature_release.description.board", - "translation": "使用会议安排面板保持会议一起正常。使用项目任务面板管理你的工作量。" + "id": "app.plugin.skip_installation.app_error", + "translation": "由于现有版本相等或更新,跳过插件 {{.Id}} 的安装。" }, { - "id": "worktemplate.product_teams.bug_bash.integration", - "translation": "通过集成大多您使用过的工具(比如Jira)在您的频道里提高生产力,跟踪你的Bug解决过程。这些工具将为你下载。" + "id": "app.post.delete_post.get_team.app_error", + "translation": "获取团队时发生错误。" }, { - "id": "worktemplate.leadership.goals_and_okrs.integration", - "translation": "通过集成大多您使用过的工具(比如 Zoom)在您的频道里提高生产力,实行更容易的协作。这些工具将为你下载。" + "id": "ent.elasticsearch.max_version.app_error", + "translation": "Elasticsearch 版本 {{.Version}} 高于最大支持版本 {{.MaxVersion}}" }, { - "id": "worktemplate.leadership.goals_and_okrs.channel", - "translation": "和您的团队讨论目标和进度,以异步或者同步的方式,在同一个频道中保持跟上任何发生的变化。" + "id": "model.config.is_valid.persistent_notifications_recipients.app_error", + "translation": "持久通知的最大收件人数无效。必须是正数。" }, { - "id": "worktemplate.leadership.goals_and_okrs.board", - "translation": "使用目标和OKR面板跟踪您的团队进度向组织目标推进。使用会议安排面板保持会议一起正常。" + "id": "api.command_exportlink.desc", + "translation": "生成一个下载导出文件的链接。" }, { - "id": "worktemplate.devops.incident_resolution.description.channel", - "translation": "在一单独频道与您的团队讨论优先级、添加利益相关者,提供更新,向解决的方向努力。" + "id": "api.command_exportlink.driver.app_error", + "translation": "文件存储驱动程序不支持链接生成。" }, { - "id": "worktemplate.devops.incident_resolution.description.board", - "translation": "使用事故解决面板来实现重复性的流程和分配跨越整个团队的任务。" + "id": "api.command_exportlink.empty.app_error", + "translation": "未找到导出文件。" }, { - "id": "worktemplate.companywide.goals_and_okrs.integration", - "translation": "通过集成大多您使用过的工具(比如 Zoom)在您的频道里提高生产力,实行更容易的协作。这些工具将为你下载。" + "id": "api.command_exportlink.hint", + "translation": "[job-id|zip 文件名|{{.LatestMsg}}]" }, { - "id": "worktemplate.companywide.goals_and_okrs.channel", - "translation": "和您的团队讨论目标和进度,以异步或者同步的方式,在同一个频道中保持跟上任何发生的变化。" + "id": "api.command_exportlink.invalid.app_error", + "translation": "无法找到您请求的文件。" }, { - "id": "worktemplate.companywide.goals_and_okrs.board", - "translation": "使用目标和OKR面板跟踪您的团队进度向组织目标推进。使用会议安排面板保持会议一起正常。" + "id": "api.command_exportlink.link.text", + "translation": "您可以在此处下载您的文件:{{.Link}}。\n此链接将在 {{.Expiration}} 后过期。" }, { - "id": "worktemplate.companywide.create_project.integration", - "translation": "通过集成大多您使用过的工具在您的频道里提高生产力。这些工具将为你下载。" + "id": "api.command_exportlink.list.app_error", + "translation": "无法检索导出列表。" }, { - "id": "worktemplate.companywide.create_project.channel", - "translation": "与您的团队在一个协作频道里讨论新的项目并且决定如何你们将如此组织。" + "id": "api.command_exportlink.name", + "translation": "exportlink" }, { - "id": "worktemplate.companywide.create_project.board", - "translation": "使用看板面板定义和跟踪您的项目任务列表和进度。" + "id": "api.command_exportlink.permission.app_error", + "translation": "您没有足够的权限运行此命令。" + }, + { + "id": "api.command_exportlink.presign.app_error", + "translation": "无法生成预签名 URL。" + }, + { + "id": "app.channel.get_channels_member_count.existing.app_error", + "translation": "无法找到给定频道的成员计数。" + }, + { + "id": "app.channel.get_channels_member_count.find.app_error", + "translation": "无法找到成员计数。" + }, + { + "id": "app.post_priority.delete_persistent_notification_post.app_error", + "translation": "删除持久通知帖子失败" + }, + { + "id": "app.channel.update_member.app_error", + "translation": "无法更新频道成员。" + }, + { + "id": "app.channel.update_member.notify_props_limit_exceeded.app_error", + "translation": "无法更新频道成员,通知属性大小超过限制。" + }, + { + "id": "app.eport.generate_presigned_url.config.app_error", + "translation": "此操作需要使用专用的导出存储。" + }, + { + "id": "app.eport.generate_presigned_url.driver.app_error", + "translation": "您的导出存储驱动程序不支持预签名 URL 生成。" + }, + { + "id": "app.eport.generate_presigned_url.featureflag.app_error", + "translation": "此功能受功能标志限制。" + }, + { + "id": "app.eport.generate_presigned_url.fileexist.app_error", + "translation": "无法检查文件是否存在。" + }, + { + "id": "app.eport.generate_presigned_url.link.app_error", + "translation": "无法生成预签名 URL。" + }, + { + "id": "app.eport.generate_presigned_url.notfound.app_error", + "translation": "未找到导出文件。" + }, + { + "id": "app.file_info.get.gif.app_error", + "translation": "无法解码 GIF。" + }, + { + "id": "model.license_record.is_valid.bytes.app_error", + "translation": "上传许可证时字节的值无效。" + }, + { + "id": "model.config.is_valid.local_mode_socket.app_error", + "translation": "无法找到本地套接字文件目录。" + }, + { + "id": "app.post_persistent_notification.delete_by_channel.app_error", + "translation": "无法通过频道删除持久通知。" + }, + { + "id": "app.post_persistent_notification.delete_by_team.app_error", + "translation": "无法通过团队删除持久通知。" + }, + { + "id": "app.save_config.plugin_hook_error", + "translation": "在保存配置时运行插件钩子时发生错误。" + }, + { + "id": "model.channel_member.is_valid.channel_auto_follow_threads_value.app_error", + "translation": "channel-auto-follow-threads 值无效。" + }, + { + "id": "model.channel_member.is_valid.notify_props.app_error", + "translation": "超过通知属性大小限制。" + }, + { + "id": "model.config.is_valid.elastic_search.ignored_indexes_dash_prefix.app_error", + "translation": "忽略的索引清除不应以破折号开头。" + }, + { + "id": "model.config.is_valid.persistent_notifications_count.app_error", + "translation": "持久通知每个帖子的总数无效。必须是正数。" + }, + { + "id": "model.config.is_valid.persistent_notifications_interval.app_error", + "translation": "持久通知的频率无效。必须至少为两分钟。" + }, + { + "id": "api.server.cws.subscribe_to_newsletter.app_error", + "translation": "CWS 服务器订阅通讯失败。" + }, + { + "id": "api.post.post_priority.max_recipients_persistent_notification_post.request_error", + "translation": "持久通知帖子最多允许 {{.MaxRecipients}} 个收件人。" + }, + { + "id": "api.post.post_priority.min_recipients_persistent_notification_post.request_error", + "translation": "持久通知帖子必须至少有 1 个收件人。" + }, + { + "id": "api.post.post_priority.persistent_notification_validation_error.request_error", + "translation": "持久通知验证失败。" + }, + { + "id": "api.post.post_priority.priority_post_not_allowed_for_user.request_error", + "translation": "用户不允许创建优先级帖子或持久通知。" + }, + { + "id": "api.post.post_priority.priority_post_only_allowed_for_root_post.request_error", + "translation": "只有根帖子可以具有优先级。" + }, + { + "id": "api.post.post_priority.urgent_persistent_notification_post.request_error", + "translation": "持久通知帖子必须具有紧急优先级。" + }, + { + "id": "api.user.invalidate_password_recovery_tokens.error", + "translation": "在使密码恢复令牌失效时无法按类型获取令牌" + }, + { + "id": "api.user.invalidate_password_recovery_tokens_delete.error", + "translation": "在使密码恢复令牌失效时无法删除令牌" + }, + { + "id": "api.user.invalidate_password_recovery_tokens_parse.error", + "translation": "在使密码恢复令牌失效时无法解析令牌" } ] diff --git a/server/i18n/zh-TW.json b/server/i18n/zh-TW.json index 4f759a42903..1864b714b62 100644 --- a/server/i18n/zh-TW.json +++ b/server/i18n/zh-TW.json @@ -4035,10 +4035,6 @@ "id": "model.emoji.user_id.app_error", "translation": "無效的建立者 ID" }, - { - "id": "model.file_info.get.gif.app_error", - "translation": "無法將 gif 解碼." - }, { "id": "model.file_info.is_valid.create_at.app_error", "translation": "無效的建立時間" diff --git a/server/platform/services/httpservice/client.go b/server/platform/services/httpservice/client.go index bbfca0da9b3..1aca082cc0b 100644 --- a/server/platform/services/httpservice/client.go +++ b/server/platform/services/httpservice/client.go @@ -89,7 +89,7 @@ func init() { type DialContextFunction func(ctx context.Context, network, addr string) (net.Conn, error) -var AddressForbidden = errors.New("address forbidden, you may need to set AllowedUntrustedInternalConnections to allow an integration access to your internal network") +var ErrAddressForbidden = errors.New("address forbidden, you may need to set AllowedUntrustedInternalConnections to allow an integration access to your internal network") func dialContextFilter(dial DialContextFunction, allowHost func(host string) bool, allowIP func(ip net.IP) bool) DialContextFunction { return func(ctx context.Context, network, addr string) (net.Conn, error) { @@ -128,7 +128,7 @@ func dialContextFilter(dial DialContextFunction, allowHost func(host string) boo } } if firstErr == nil { - return nil, AddressForbidden + return nil, ErrAddressForbidden } return nil, firstErr } diff --git a/server/platform/services/httpservice/client_test.go b/server/platform/services/httpservice/client_test.go index a274ebf5f35..4d35b0ac164 100644 --- a/server/platform/services/httpservice/client_test.go +++ b/server/platform/services/httpservice/client_test.go @@ -89,7 +89,7 @@ func TestHTTPClient(t *testing.T) { require.NoError(t, err) } else { require.IsType(t, &url.Error{}, err) - require.Equal(t, AddressForbidden, err.(*url.Error).Err) + require.Equal(t, ErrAddressForbidden, err.(*url.Error).Err) } }) } @@ -154,7 +154,7 @@ func TestDialContextFilter(t *testing.T) { require.True(t, didDial) } else { require.Error(t, err) - require.Equal(t, err, AddressForbidden) + require.Equal(t, err, ErrAddressForbidden) require.False(t, didDial) } } diff --git a/server/platform/services/imageproxy/imageproxy.go b/server/platform/services/imageproxy/imageproxy.go index f9e5d6e13f8..2737691e681 100644 --- a/server/platform/services/imageproxy/imageproxy.go +++ b/server/platform/services/imageproxy/imageproxy.go @@ -124,7 +124,7 @@ func (proxy *ImageProxy) GetImageDirect(imageURL string) (io.ReadCloser, string, // GetProxiedImageURL takes the URL of an image and returns a URL that can be used to view that image through the // image proxy. func (proxy *ImageProxy) GetProxiedImageURL(imageURL string) string { - if imageURL == "" || proxy.siteURL == nil { + if imageURL == "" || proxy.siteURL == nil || strings.HasPrefix(strings.ToLower(imageURL), "data:image/") { return imageURL } // Parse url, return siteURL in case of failure. diff --git a/server/platform/services/imageproxy/imageproxy_test.go b/server/platform/services/imageproxy/imageproxy_test.go index 4f0b816ce43..dcd21b0060d 100644 --- a/server/platform/services/imageproxy/imageproxy_test.go +++ b/server/platform/services/imageproxy/imageproxy_test.go @@ -66,6 +66,11 @@ func TestGetProxiedImageURL(t *testing.T) { Input: "https://mattermost.example.com@anothersite.com/static/logo.png", Expected: "https://mattermost.example.com/api/v4/image?url=https%3A%2F%2Fmattermost.example.com%40anothersite.com%2Fstatic%2Flogo.png", }, + { + Name: "should not proxy embedded image", + Input: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==", + Expected: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==", + }, } { t.Run(test.Name, func(t *testing.T) { assert.Equal(t, test.Expected, proxy.GetProxiedImageURL(test.Input)) diff --git a/server/platform/services/imageproxy/local.go b/server/platform/services/imageproxy/local.go index d1ac69c8c15..dba0d80f2d1 100644 --- a/server/platform/services/imageproxy/local.go +++ b/server/platform/services/imageproxy/local.go @@ -152,13 +152,12 @@ func (backend *LocalBackend) ServeImage(w http.ResponseWriter, req *http.Request resp, err := backend.client.Do(actualReq) if err != nil { - msg := fmt.Sprintf("error fetching remote image: %v", err) - mlog.Warn(msg) + mlog.Warn("error fetching remote image", mlog.Err(err)) statusCode := http.StatusInternalServerError if e, ok := err.(net.Error); ok && e.Timeout() { statusCode = http.StatusGatewayTimeout } - http.Error(w, msg, statusCode) + http.Error(w, fmt.Sprintf("error fetching remote image: %v", err), statusCode) return } // close the original resp.Body, even if we wrap it in a NopCloser below diff --git a/server/platform/services/remotecluster/mocks_test.go b/server/platform/services/remotecluster/mocks_test.go index 37c7d24b8a3..e45f70a53b7 100644 --- a/server/platform/services/remotecluster/mocks_test.go +++ b/server/platform/services/remotecluster/mocks_test.go @@ -5,6 +5,7 @@ package remotecluster import ( "context" + "testing" "github.com/mattermost/mattermost/server/public/model" "github.com/mattermost/mattermost/server/public/plugin/plugintest/mock" @@ -20,12 +21,12 @@ type mockServer struct { user *model.User } -func newMockServer(remotes []*model.RemoteCluster) *mockServer { - testLogger := mlog.CreateConsoleTestLogger(true, mlog.LvlDebug) +func newMockServer(t *testing.T, remotes []*model.RemoteCluster) *mockServer { + logger := mlog.CreateConsoleTestLogger(t) return &mockServer{ remotes: remotes, - logger: testLogger, + logger: logger, } } @@ -59,4 +60,3 @@ func (ms *mockServer) GetStore() store.Store { storeMock.On("User").Return(userStoreMock) return storeMock } -func (ms *mockServer) Shutdown() { ms.logger.Shutdown() } diff --git a/server/platform/services/remotecluster/ping_test.go b/server/platform/services/remotecluster/ping_test.go index 73978b03bbc..df729683bbb 100644 --- a/server/platform/services/remotecluster/ping_test.go +++ b/server/platform/services/remotecluster/ping_test.go @@ -66,8 +66,7 @@ func TestPing(t *testing.T) { })) defer ts.Close() - mockServer := newMockServer(makeRemoteClusters(NumRemotes, ts.URL)) - defer mockServer.Shutdown() + mockServer := newMockServer(t, makeRemoteClusters(NumRemotes, ts.URL)) service, err := NewRemoteClusterService(mockServer) require.NoError(t, err) @@ -116,8 +115,7 @@ func TestPing(t *testing.T) { })) defer ts.Close() - mockServer := newMockServer(makeRemoteClusters(NumRemotes, ts.URL)) - defer mockServer.Shutdown() + mockServer := newMockServer(t, makeRemoteClusters(NumRemotes, ts.URL)) service, err := NewRemoteClusterService(mockServer) require.NoError(t, err) diff --git a/server/platform/services/remotecluster/send_test.go b/server/platform/services/remotecluster/send_test.go index 9d3a3921736..360d557c628 100644 --- a/server/platform/services/remotecluster/send_test.go +++ b/server/platform/services/remotecluster/send_test.go @@ -82,8 +82,7 @@ func TestBroadcastMsg(t *testing.T) { })) defer ts.Close() - mockServer := newMockServer(makeRemoteClusters(NumRemotes, ts.URL)) - defer mockServer.Shutdown() + mockServer := newMockServer(t, makeRemoteClusters(NumRemotes, ts.URL)) service, err := NewRemoteClusterService(mockServer) require.NoError(t, err) @@ -139,8 +138,7 @@ func TestBroadcastMsg(t *testing.T) { })) defer ts.Close() - mockServer := newMockServer(makeRemoteClusters(NumRemotes, ts.URL)) - defer mockServer.Shutdown() + mockServer := newMockServer(t, makeRemoteClusters(NumRemotes, ts.URL)) service, err := NewRemoteClusterService(mockServer) require.NoError(t, err) diff --git a/server/platform/services/remotecluster/sendprofileImage_test.go b/server/platform/services/remotecluster/sendprofileImage_test.go index a200297332e..ab47234796a 100644 --- a/server/platform/services/remotecluster/sendprofileImage_test.go +++ b/server/platform/services/remotecluster/sendprofileImage_test.go @@ -100,8 +100,7 @@ func TestService_sendProfileImageToRemote(t *testing.T) { provider := testImageProvider{} - mockServer := newMockServer(makeRemoteClusters(NumRemotes, ts.URL)) - defer mockServer.Shutdown() + mockServer := newMockServer(t, makeRemoteClusters(NumRemotes, ts.URL)) mockServer.SetUser(user) service, err := NewRemoteClusterService(mockServer) require.NoError(t, err) diff --git a/server/platform/services/remotecluster/service_test.go b/server/platform/services/remotecluster/service_test.go index 9e20377c757..9773a244444 100644 --- a/server/platform/services/remotecluster/service_test.go +++ b/server/platform/services/remotecluster/service_test.go @@ -29,8 +29,7 @@ func TestService_AddTopicListener(t *testing.T) { return nil } - mockServer := newMockServer(makeRemoteClusters(NumRemotes, "")) - defer mockServer.Shutdown() + mockServer := newMockServer(t, makeRemoteClusters(NumRemotes, "")) service, err := NewRemoteClusterService(mockServer) require.NoError(t, err) diff --git a/server/platform/services/searchengine/bleveengine/indexer/indexing_job.go b/server/platform/services/searchengine/bleveengine/indexer/indexing_job.go index 58110842b9b..db73517f275 100644 --- a/server/platform/services/searchengine/bleveengine/indexer/indexing_job.go +++ b/server/platform/services/searchengine/bleveengine/indexer/indexing_job.go @@ -12,6 +12,7 @@ import ( "github.com/mattermost/mattermost/server/public/model" "github.com/mattermost/mattermost/server/public/shared/mlog" + "github.com/mattermost/mattermost/server/public/shared/request" "github.com/mattermost/mattermost/server/v8/channels/jobs" "github.com/mattermost/mattermost/server/v8/platform/services/searchengine/bleveengine" ) @@ -31,20 +32,23 @@ type BleveIndexerWorker struct { stopped chan bool jobs chan model.Job jobServer *jobs.JobServer + logger mlog.LoggerIFace engine *bleveengine.BleveEngine closed int32 } -func MakeWorker(jobServer *jobs.JobServer, engine *bleveengine.BleveEngine) model.Worker { +func MakeWorker(jobServer *jobs.JobServer, engine *bleveengine.BleveEngine) *BleveIndexerWorker { if engine == nil { return nil } + const workerName = "BleveIndexer" return &BleveIndexerWorker{ - name: "BleveIndexer", + name: workerName, stop: make(chan struct{}), stopped: make(chan bool, 1), jobs: make(chan model.Job), jobServer: jobServer, + logger: jobServer.Logger().With(mlog.String("workername", workerName)), engine: engine, } } @@ -97,20 +101,22 @@ func (worker *BleveIndexerWorker) Run() { if atomic.CompareAndSwapInt32(&worker.closed, 1, 0) { worker.stop = make(chan struct{}) } - mlog.Debug("Worker Started", mlog.String("workername", worker.name)) + worker.logger.Debug("Worker Started") defer func() { - mlog.Debug("Worker: Finished", mlog.String("workername", worker.name)) + worker.logger.Debug("Worker: Finished") worker.stopped <- true }() for { select { case <-worker.stop: - mlog.Debug("Worker: Received stop signal", mlog.String("workername", worker.name)) + worker.logger.Debug("Worker: Received stop signal") return case job := <-worker.jobs: - mlog.Debug("Worker: Received a new candidate job.", mlog.String("workername", worker.name)) + job.Logger = job.Logger.With(mlog.String("workername", worker.name)) + + job.Logger.Debug("Worker: Received a new candidate job.") worker.DoJob(&job) } } @@ -121,7 +127,7 @@ func (worker *BleveIndexerWorker) Stop() { if !atomic.CompareAndSwapInt32(&worker.closed, 0, 1) { return } - mlog.Debug("Worker Stopping", mlog.String("workername", worker.name)) + worker.logger.Debug("Worker Stopping") close(worker.stop) <-worker.stopped } @@ -129,19 +135,19 @@ func (worker *BleveIndexerWorker) Stop() { func (worker *BleveIndexerWorker) DoJob(job *model.Job) { claimed, err := worker.jobServer.ClaimJob(job) if err != nil { - mlog.Warn("Worker: Error occurred while trying to claim job", mlog.String("workername", worker.name), mlog.String("job_id", job.Id), mlog.Err(err)) + job.Logger.Warn("Worker: Error occurred while trying to claim job", mlog.Err(err)) return } if !claimed { return } - mlog.Info("Worker: Indexing job claimed by worker", mlog.String("workername", worker.name), mlog.String("job_id", job.Id)) + job.Logger.Info("Worker: Indexing job claimed by worker") if !worker.engine.IsActive() { appError := model.NewAppError("BleveIndexerWorker", "bleveengine.indexer.do_job.engine_inactive", nil, "", http.StatusInternalServerError) if err := worker.jobServer.SetJobError(job, appError); err != nil { - mlog.Error("Worker: Failed to run job as ") + job.Logger.Error("Worker: Failed to run job as ") } return } @@ -160,10 +166,10 @@ func (worker *BleveIndexerWorker) DoJob(job *model.Job) { if startString, ok := job.Data["start_time"]; ok { startInt, err := strconv.ParseInt(startString, 10, 64) if err != nil { - mlog.Error("Worker: Failed to parse start_time for job", mlog.String("workername", worker.name), mlog.String("start_time", startString), mlog.String("job_id", job.Id), mlog.Err(err)) + job.Logger.Error("Worker: Failed to parse start_time for job", mlog.String("start_time", startString), mlog.Err(err)) appError := model.NewAppError("BleveIndexerWorker", "bleveengine.indexer.do_job.parse_start_time.error", nil, "", http.StatusInternalServerError).Wrap(err) if err := worker.jobServer.SetJobError(job, appError); err != nil { - mlog.Error("Worker: Failed to set job error", mlog.String("workername", worker.name), mlog.String("job_id", job.Id), mlog.Err(err), mlog.NamedErr("set_error", appError)) + job.Logger.Error("Worker: Failed to set job error", mlog.Err(err), mlog.NamedErr("set_error", appError)) } return } @@ -173,10 +179,10 @@ func (worker *BleveIndexerWorker) DoJob(job *model.Job) { // A user or a channel may be created before any post. oldestEntityCreationTime, err := worker.jobServer.Store.Post().GetOldestEntityCreationTime() if err != nil { - mlog.Error("Worker: Failed to fetch oldest entity for job.", mlog.String("workername", worker.name), mlog.String("job_id", job.Id), mlog.String("start_time", startString), mlog.Err(err)) + job.Logger.Error("Worker: Failed to fetch oldest entity for job.", mlog.String("start_time", startString), mlog.Err(err)) appError := model.NewAppError("BleveIndexerWorker", "bleveengine.indexer.do_job.get_oldest_entity.error", nil, "", http.StatusInternalServerError).Wrap(err) if err := worker.jobServer.SetJobError(job, appError); err != nil { - mlog.Error("Worker: Failed to set job error", mlog.String("workername", worker.name), mlog.String("job_id", job.Id), mlog.Err(err), mlog.NamedErr("set_error", appError)) + job.Logger.Error("Worker: Failed to set job error", mlog.Err(err), mlog.NamedErr("set_error", appError)) } return } @@ -187,10 +193,10 @@ func (worker *BleveIndexerWorker) DoJob(job *model.Job) { if endString, ok := job.Data["end_time"]; ok { endInt, err := strconv.ParseInt(endString, 10, 64) if err != nil { - mlog.Error("Worker: Failed to parse end_time for job", mlog.String("workername", worker.name), mlog.String("job_id", job.Id), mlog.String("end_time", endString), mlog.Err(err)) + job.Logger.Error("Worker: Failed to parse end_time for job", mlog.String("end_time", endString), mlog.Err(err)) appError := model.NewAppError("BleveIndexerWorker", "bleveengine.indexer.do_job.parse_end_time.error", nil, "", http.StatusInternalServerError).Wrap(err) if err := worker.jobServer.SetJobError(job, appError); err != nil { - mlog.Error("Worker: Failed to set job errorv", mlog.String("workername", worker.name), mlog.String("job_id", job.Id), mlog.Err(err), mlog.NamedErr("set_error", appError)) + job.Logger.Error("Worker: Failed to set job errorv", mlog.Err(err), mlog.NamedErr("set_error", appError)) } return } @@ -213,7 +219,7 @@ func (worker *BleveIndexerWorker) DoJob(job *model.Job) { // Counting all posts may fail or timeout when the posts table is large. If this happens, log a warning, but carry // on with the indexing job anyway. The only issue is that the progress % reporting will be inaccurate. if count, err := worker.jobServer.Store.Post().AnalyticsPostCount(&model.PostCountOptions{}); err != nil { - mlog.Warn("Worker: Failed to fetch total post count for job. An estimated value will be used for progress reporting.", mlog.String("workername", worker.name), mlog.String("job_id", job.Id), mlog.Err(err)) + job.Logger.Warn("Worker: Failed to fetch total post count for job. An estimated value will be used for progress reporting.", mlog.Err(err)) progress.TotalPostsCount = estimatedPostCount } else { progress.TotalPostsCount = count @@ -221,7 +227,7 @@ func (worker *BleveIndexerWorker) DoJob(job *model.Job) { // Same possible fail as above can happen when counting channels if count, err := worker.jobServer.Store.Channel().AnalyticsTypeCount("", ""); err != nil { - mlog.Warn("Worker: Failed to fetch total channel count for job. An estimated value will be used for progress reporting.", mlog.String("workername", worker.name), mlog.String("job_id", job.Id), mlog.Err(err)) + job.Logger.Warn("Worker: Failed to fetch total channel count for job. An estimated value will be used for progress reporting.", mlog.Err(err)) progress.TotalChannelsCount = estimatedChannelCount } else { progress.TotalChannelsCount = count @@ -232,7 +238,7 @@ func (worker *BleveIndexerWorker) DoJob(job *model.Job) { IncludeBotAccounts: true, // This actually doesn't join with the bots table // since ExcludeRegularUsers is set to false }); err != nil { - mlog.Warn("Worker: Failed to fetch total user count for job. An estimated value will be used for progress reporting.", mlog.String("workername", worker.name), mlog.String("job_id", job.Id), mlog.Err(err)) + job.Logger.Warn("Worker: Failed to fetch total user count for job. An estimated value will be used for progress reporting.", mlog.Err(err)) progress.TotalUsersCount = estimatedUserCount } else { progress.TotalUsersCount = count @@ -241,40 +247,41 @@ func (worker *BleveIndexerWorker) DoJob(job *model.Job) { // Counting all files may fail or timeout when the file_info table is large. If this happens, log a warning, but carry // on with the indexing job anyway. The only issue is that the progress % reporting will be inaccurate. if count, err := worker.jobServer.Store.FileInfo().CountAll(); err != nil { - mlog.Warn("Worker: Failed to fetch total file info count for job. An estimated value will be used for progress reporting.", mlog.String("workername", worker.name), mlog.String("job_id", job.Id), mlog.Err(err)) + job.Logger.Warn("Worker: Failed to fetch total file info count for job. An estimated value will be used for progress reporting.", mlog.Err(err)) progress.TotalFilesCount = estimatedFilesCount } else { progress.TotalFilesCount = count } + cancelContext := request.EmptyContext(worker.logger) cancelCtx, cancelCancelWatcher := context.WithCancel(context.Background()) cancelWatcherChan := make(chan struct{}, 1) - go worker.jobServer.CancellationWatcher(cancelCtx, job.Id, cancelWatcherChan) - + cancelContext.SetContext(cancelCtx) + go worker.jobServer.CancellationWatcher(cancelContext, job.Id, cancelWatcherChan) defer cancelCancelWatcher() for { select { case <-cancelWatcherChan: - mlog.Info("Worker: Indexing job has been canceled via CancellationWatcher", mlog.String("workername", worker.name), mlog.String("job_id", job.Id)) + job.Logger.Info("Worker: Indexing job has been canceled via CancellationWatcher") if err := worker.jobServer.SetJobCanceled(job); err != nil { - mlog.Error("Worker: Failed to mark job as cancelled", mlog.String("workername", worker.name), mlog.String("job_id", job.Id), mlog.Err(err)) + job.Logger.Error("Worker: Failed to mark job as cancelled", mlog.Err(err)) } return case <-worker.stop: - mlog.Info("Worker: Indexing has been canceled via Worker Stop", mlog.String("workername", worker.name), mlog.String("job_id", job.Id)) + job.Logger.Info("Worker: Indexing has been canceled via Worker Stop") if err := worker.jobServer.SetJobCanceled(job); err != nil { - mlog.Error("Worker: Failed to mark job as canceled", mlog.String("workername", worker.name), mlog.String("job_id", job.Id), mlog.Err(err)) + job.Logger.Error("Worker: Failed to mark job as canceled", mlog.Err(err)) } return case <-time.After(timeBetweenBatches): var err *model.AppError - if progress, err = worker.IndexBatch(progress); err != nil { - mlog.Error("Worker: Failed to index batch for job", mlog.String("workername", worker.name), mlog.String("job_id", job.Id), mlog.Err(err)) + if progress, err = worker.IndexBatch(job.Logger, progress); err != nil { + job.Logger.Error("Worker: Failed to index batch for job", mlog.Err(err)) if err2 := worker.jobServer.SetJobError(job, err); err2 != nil { - mlog.Error("Worker: Failed to set job error", mlog.String("workername", worker.name), mlog.String("job_id", job.Id), mlog.Err(err2), mlog.NamedErr("set_error", err)) + job.Logger.Error("Worker: Failed to set job error", mlog.Err(err2), mlog.NamedErr("set_error", err)) } return } @@ -293,44 +300,44 @@ func (worker *BleveIndexerWorker) DoJob(job *model.Job) { job.Data["end_time"] = strconv.FormatInt(progress.EndAtTime, 10) if err := worker.jobServer.SetJobProgress(job, progress.CurrentProgress()); err != nil { - mlog.Error("Worker: Failed to set progress for job", mlog.String("workername", worker.name), mlog.String("job_id", job.Id), mlog.Err(err)) + job.Logger.Error("Worker: Failed to set progress for job", mlog.Err(err)) if err2 := worker.jobServer.SetJobError(job, err); err2 != nil { - mlog.Error("Worker: Failed to set error for job", mlog.String("workername", worker.name), mlog.String("job_id", job.Id), mlog.Err(err2), mlog.NamedErr("set_error", err)) + job.Logger.Error("Worker: Failed to set error for job", mlog.Err(err2), mlog.NamedErr("set_error", err)) } return } if progress.IsDone() { if err := worker.jobServer.SetJobSuccess(job); err != nil { - mlog.Error("Worker: Failed to set success for job", mlog.String("workername", worker.name), mlog.String("job_id", job.Id), mlog.Err(err)) + job.Logger.Error("Worker: Failed to set success for job", mlog.Err(err)) if err2 := worker.jobServer.SetJobError(job, err); err2 != nil { - mlog.Error("Worker: Failed to set error for job", mlog.String("workername", worker.name), mlog.String("job_id", job.Id), mlog.Err(err2), mlog.NamedErr("set_error", err)) + job.Logger.Error("Worker: Failed to set error for job", mlog.Err(err2), mlog.NamedErr("set_error", err)) } } - mlog.Info("Worker: Indexing job finished successfully", mlog.String("workername", worker.name), mlog.String("job_id", job.Id)) + job.Logger.Info("Worker: Indexing job finished successfully") return } } } } -func (worker *BleveIndexerWorker) IndexBatch(progress IndexingProgress) (IndexingProgress, *model.AppError) { +func (worker *BleveIndexerWorker) IndexBatch(logger mlog.LoggerIFace, progress IndexingProgress) (IndexingProgress, *model.AppError) { if !progress.DonePosts { - return worker.IndexPostsBatch(progress) + return worker.IndexPostsBatch(logger, progress) } if !progress.DoneChannels { - return worker.IndexChannelsBatch(progress) + return worker.IndexChannelsBatch(logger, progress) } if !progress.DoneUsers { - return worker.IndexUsersBatch(progress) + return worker.IndexUsersBatch(logger, progress) } if !progress.DoneFiles { - return worker.IndexFilesBatch(progress) + return worker.IndexFilesBatch(logger, progress) } return progress, model.NewAppError("BleveIndexerWorker", "bleveengine.indexer.index_batch.nothing_left_to_index.error", nil, "", http.StatusInternalServerError) } -func (worker *BleveIndexerWorker) IndexPostsBatch(progress IndexingProgress) (IndexingProgress, *model.AppError) { +func (worker *BleveIndexerWorker) IndexPostsBatch(logger mlog.LoggerIFace, progress IndexingProgress) (IndexingProgress, *model.AppError) { var posts []*model.PostForIndexing tries := 0 @@ -341,7 +348,7 @@ func (worker *BleveIndexerWorker) IndexPostsBatch(progress IndexingProgress) (In if tries >= 10 { return progress, model.NewAppError("IndexPostsBatch", "app.post.get_posts_batch_for_indexing.get.app_error", nil, "", http.StatusInternalServerError).Wrap(err) } - mlog.Warn("Failed to get posts batch for indexing. Retrying.", mlog.Err(err)) + logger.Warn("Failed to get posts batch for indexing. Retrying.", mlog.Err(err)) // Wait a bit before trying again. time.Sleep(15 * time.Second) @@ -398,7 +405,7 @@ func (worker *BleveIndexerWorker) BulkIndexPosts(posts []*model.PostForIndexing, return &posts[len(posts)-1].Post, nil } -func (worker *BleveIndexerWorker) IndexFilesBatch(progress IndexingProgress) (IndexingProgress, *model.AppError) { +func (worker *BleveIndexerWorker) IndexFilesBatch(logger mlog.LoggerIFace, progress IndexingProgress) (IndexingProgress, *model.AppError) { var files []*model.FileForIndexing tries := 0 @@ -409,7 +416,7 @@ func (worker *BleveIndexerWorker) IndexFilesBatch(progress IndexingProgress) (In if tries >= 10 { return progress, model.NewAppError("IndexFilesBatch", "app.post.get_files_batch_for_indexing.get.app_error", nil, "", http.StatusInternalServerError).Wrap(err) } - mlog.Warn("Failed to get files batch for indexing. Retrying.", mlog.Err(err)) + logger.Warn("Failed to get files batch for indexing. Retrying.", mlog.Err(err)) // Wait a bit before trying again. time.Sleep(15 * time.Second) @@ -465,7 +472,7 @@ func (worker *BleveIndexerWorker) BulkIndexFiles(files []*model.FileForIndexing, return &files[len(files)-1].FileInfo, nil } -func (worker *BleveIndexerWorker) IndexChannelsBatch(progress IndexingProgress) (IndexingProgress, *model.AppError) { +func (worker *BleveIndexerWorker) IndexChannelsBatch(logger mlog.LoggerIFace, progress IndexingProgress) (IndexingProgress, *model.AppError) { var channels []*model.Channel tries := 0 @@ -477,7 +484,7 @@ func (worker *BleveIndexerWorker) IndexChannelsBatch(progress IndexingProgress) return progress, model.NewAppError("BleveIndexerWorker.IndexChannelsBatch", "app.channel.get_channels_batch_for_indexing.get.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr) } - mlog.Warn("Failed to get channels batch for indexing. Retrying.", mlog.Err(nErr)) + logger.Warn("Failed to get channels batch for indexing. Retrying.", mlog.Err(nErr)) // Wait a bit before trying again. time.Sleep(15 * time.Second) @@ -491,7 +498,7 @@ func (worker *BleveIndexerWorker) IndexChannelsBatch(progress IndexingProgress) return progress, nil } - lastChannel, err := worker.BulkIndexChannels(channels, progress) + lastChannel, err := worker.BulkIndexChannels(logger, channels, progress) if err != nil { return progress, err } @@ -511,7 +518,7 @@ func (worker *BleveIndexerWorker) IndexChannelsBatch(progress IndexingProgress) return progress, nil } -func (worker *BleveIndexerWorker) BulkIndexChannels(channels []*model.Channel, progress IndexingProgress) (*model.Channel, *model.AppError) { +func (worker *BleveIndexerWorker) BulkIndexChannels(logger mlog.LoggerIFace, channels []*model.Channel, progress IndexingProgress) (*model.Channel, *model.AppError) { batch := worker.engine.ChannelIndex.NewBatch() for _, channel := range channels { @@ -547,7 +554,7 @@ func (worker *BleveIndexerWorker) BulkIndexChannels(channels []*model.Channel, p return channels[len(channels)-1], nil } -func (worker *BleveIndexerWorker) IndexUsersBatch(progress IndexingProgress) (IndexingProgress, *model.AppError) { +func (worker *BleveIndexerWorker) IndexUsersBatch(logger mlog.LoggerIFace, progress IndexingProgress) (IndexingProgress, *model.AppError) { var users []*model.UserForIndexing tries := 0 @@ -556,7 +563,7 @@ func (worker *BleveIndexerWorker) IndexUsersBatch(progress IndexingProgress) (In if tries >= 10 { return progress, model.NewAppError("IndexUsersBatch", "app.user.get_users_batch_for_indexing.get_users.app_error", nil, "", http.StatusInternalServerError).Wrap(err) } - mlog.Warn("Failed to get users batch for indexing. Retrying.", mlog.Err(err)) + logger.Warn("Failed to get users batch for indexing. Retrying.", mlog.Err(err)) // Wait a bit before trying again. time.Sleep(15 * time.Second) @@ -573,7 +580,7 @@ func (worker *BleveIndexerWorker) IndexUsersBatch(progress IndexingProgress) (In return progress, nil } - lastUser, err := worker.BulkIndexUsers(users, progress) + lastUser, err := worker.BulkIndexUsers(logger, users, progress) if err != nil { return progress, err } @@ -592,7 +599,7 @@ func (worker *BleveIndexerWorker) IndexUsersBatch(progress IndexingProgress) (In return progress, nil } -func (worker *BleveIndexerWorker) BulkIndexUsers(users []*model.UserForIndexing, progress IndexingProgress) (*model.UserForIndexing, *model.AppError) { +func (worker *BleveIndexerWorker) BulkIndexUsers(logger mlog.LoggerIFace, users []*model.UserForIndexing, progress IndexingProgress) (*model.UserForIndexing, *model.AppError) { batch := worker.engine.UserIndex.NewBatch() for _, user := range users { diff --git a/server/platform/services/searchengine/bleveengine/indexer/indexing_job_test.go b/server/platform/services/searchengine/bleveengine/indexer/indexing_job_test.go index 50f434d367b..1df114a5bef 100644 --- a/server/platform/services/searchengine/bleveengine/indexer/indexing_job_test.go +++ b/server/platform/services/searchengine/bleveengine/indexer/indexing_job_test.go @@ -11,6 +11,7 @@ import ( "github.com/stretchr/testify/require" "github.com/mattermost/mattermost/server/public/model" + "github.com/mattermost/mattermost/server/public/shared/mlog" "github.com/mattermost/mattermost/server/v8/channels/jobs" "github.com/mattermost/mattermost/server/v8/channels/store/storetest" "github.com/mattermost/mattermost/server/v8/channels/utils/testutils" @@ -22,12 +23,14 @@ func TestBleveIndexer(t *testing.T) { defer mockStore.AssertExpectations(t) t.Run("Call GetOldestEntityCreationTime for the first indexing call", func(t *testing.T) { + logger := mlog.CreateConsoleTestLogger(t) job := &model.Job{ Id: model.NewId(), CreateAt: model.GetMillis(), Status: model.JobStatusPending, Type: model.JobTypeBlevePostIndexing, } + job.InitLogger(logger) mockStore.JobStore.On("UpdateStatusOptimistically", job.Id, model.JobStatusPending, model.JobStatusInProgress).Return(true, nil) mockStore.JobStore.On("UpdateOptimistically", job, model.JobStatusInProgress).Return(true, nil) diff --git a/server/platform/services/sharedchannel/attachment.go b/server/platform/services/sharedchannel/attachment.go index b9ddfbb03be..b8671d0e662 100644 --- a/server/platform/services/sharedchannel/attachment.go +++ b/server/platform/services/sharedchannel/attachment.go @@ -12,7 +12,7 @@ import ( "github.com/mattermost/mattermost/server/public/model" "github.com/mattermost/mattermost/server/public/shared/mlog" - "github.com/mattermost/mattermost/server/v8/channels/app/request" + "github.com/mattermost/mattermost/server/public/shared/request" "github.com/mattermost/mattermost/server/v8/platform/services/remotecluster" ) diff --git a/server/platform/services/sharedchannel/channelinvite.go b/server/platform/services/sharedchannel/channelinvite.go index df83cabde5a..bad0e87eaa3 100644 --- a/server/platform/services/sharedchannel/channelinvite.go +++ b/server/platform/services/sharedchannel/channelinvite.go @@ -11,7 +11,7 @@ import ( "github.com/mattermost/mattermost/server/public/model" "github.com/mattermost/mattermost/server/public/shared/mlog" - "github.com/mattermost/mattermost/server/v8/channels/app/request" + "github.com/mattermost/mattermost/server/public/shared/request" "github.com/mattermost/mattermost/server/v8/platform/services/remotecluster" ) diff --git a/server/platform/services/sharedchannel/mock_AppIface_test.go b/server/platform/services/sharedchannel/mock_AppIface_test.go index 8579a8960af..b5ea5cc4a9d 100644 --- a/server/platform/services/sharedchannel/mock_AppIface_test.go +++ b/server/platform/services/sharedchannel/mock_AppIface_test.go @@ -10,7 +10,7 @@ import ( model "github.com/mattermost/mattermost/server/public/model" - request "github.com/mattermost/mattermost/server/v8/channels/app/request" + request "github.com/mattermost/mattermost/server/public/shared/request" ) // MockAppIface is an autogenerated mock type for the AppIface type diff --git a/server/platform/services/sharedchannel/service.go b/server/platform/services/sharedchannel/service.go index 23621b57122..13331bec5c1 100644 --- a/server/platform/services/sharedchannel/service.go +++ b/server/platform/services/sharedchannel/service.go @@ -12,7 +12,7 @@ import ( "github.com/mattermost/mattermost/server/public/model" "github.com/mattermost/mattermost/server/public/shared/mlog" - "github.com/mattermost/mattermost/server/v8/channels/app/request" + "github.com/mattermost/mattermost/server/public/shared/request" "github.com/mattermost/mattermost/server/v8/channels/store" "github.com/mattermost/mattermost/server/v8/platform/services/remotecluster" "github.com/mattermost/mattermost/server/v8/platform/shared/filestore" diff --git a/server/platform/services/sharedchannel/sync_recv.go b/server/platform/services/sharedchannel/sync_recv.go index d28ddf7d749..c1df7bfed72 100644 --- a/server/platform/services/sharedchannel/sync_recv.go +++ b/server/platform/services/sharedchannel/sync_recv.go @@ -13,7 +13,7 @@ import ( "github.com/mattermost/mattermost/server/public/model" "github.com/mattermost/mattermost/server/public/shared/mlog" - "github.com/mattermost/mattermost/server/v8/channels/app/request" + "github.com/mattermost/mattermost/server/public/shared/request" "github.com/mattermost/mattermost/server/v8/platform/services/remotecluster" ) diff --git a/server/platform/services/sharedchannel/sync_send.go b/server/platform/services/sharedchannel/sync_send.go index 9e125848a15..82a0255e7d8 100644 --- a/server/platform/services/sharedchannel/sync_send.go +++ b/server/platform/services/sharedchannel/sync_send.go @@ -11,7 +11,7 @@ import ( "github.com/mattermost/mattermost/server/public/model" "github.com/mattermost/mattermost/server/public/shared/i18n" "github.com/mattermost/mattermost/server/public/shared/mlog" - "github.com/mattermost/mattermost/server/v8/channels/app/request" + "github.com/mattermost/mattermost/server/public/shared/request" "github.com/mattermost/mattermost/server/v8/platform/services/remotecluster" ) diff --git a/server/platform/services/sharedchannel/sync_send_remote.go b/server/platform/services/sharedchannel/sync_send_remote.go index c539b796032..6da93e69c71 100644 --- a/server/platform/services/sharedchannel/sync_send_remote.go +++ b/server/platform/services/sharedchannel/sync_send_remote.go @@ -13,7 +13,7 @@ import ( "github.com/mattermost/mattermost/server/public/model" "github.com/mattermost/mattermost/server/public/shared/mlog" - "github.com/mattermost/mattermost/server/v8/channels/app/request" + "github.com/mattermost/mattermost/server/public/shared/request" "github.com/mattermost/mattermost/server/v8/platform/services/remotecluster" ) diff --git a/server/platform/services/slackimport/slackimport.go b/server/platform/services/slackimport/slackimport.go index f8d75970ad4..0f56f1c2c2d 100644 --- a/server/platform/services/slackimport/slackimport.go +++ b/server/platform/services/slackimport/slackimport.go @@ -21,7 +21,7 @@ import ( "github.com/mattermost/mattermost/server/public/model" "github.com/mattermost/mattermost/server/public/shared/i18n" "github.com/mattermost/mattermost/server/public/shared/mlog" - "github.com/mattermost/mattermost/server/v8/channels/app/request" + "github.com/mattermost/mattermost/server/public/shared/request" "github.com/mattermost/mattermost/server/v8/channels/store" "github.com/mattermost/mattermost/server/v8/channels/utils" ) @@ -143,35 +143,35 @@ func (si *SlackImporter) SlackImport(c request.CTX, fileData multipart.File, fil reader := utils.NewLimitedReaderWithError(fileReader, slackImportMaxFileSize) if file.Name == "channels.json" { publicChannels, err = slackParseChannels(reader, model.ChannelTypeOpen) - if errors.Is(err, utils.SizeLimitExceeded) { + if errors.Is(err, utils.ErrSizeLimitExceeded) { log.WriteString(i18n.T("api.slackimport.slack_import.zip.file_too_large", map[string]any{"Filename": file.Name})) continue } channels = append(channels, publicChannels...) } else if file.Name == "dms.json" { directChannels, err = slackParseChannels(reader, model.ChannelTypeDirect) - if errors.Is(err, utils.SizeLimitExceeded) { + if errors.Is(err, utils.ErrSizeLimitExceeded) { log.WriteString(i18n.T("api.slackimport.slack_import.zip.file_too_large", map[string]any{"Filename": file.Name})) continue } channels = append(channels, directChannels...) } else if file.Name == "groups.json" { privateChannels, err = slackParseChannels(reader, model.ChannelTypePrivate) - if errors.Is(err, utils.SizeLimitExceeded) { + if errors.Is(err, utils.ErrSizeLimitExceeded) { log.WriteString(i18n.T("api.slackimport.slack_import.zip.file_too_large", map[string]any{"Filename": file.Name})) continue } channels = append(channels, privateChannels...) } else if file.Name == "mpims.json" { groupChannels, err = slackParseChannels(reader, model.ChannelTypeGroup) - if errors.Is(err, utils.SizeLimitExceeded) { + if errors.Is(err, utils.ErrSizeLimitExceeded) { log.WriteString(i18n.T("api.slackimport.slack_import.zip.file_too_large", map[string]any{"Filename": file.Name})) continue } channels = append(channels, groupChannels...) } else if file.Name == "users.json" { users, err = slackParseUsers(reader) - if errors.Is(err, utils.SizeLimitExceeded) { + if errors.Is(err, utils.ErrSizeLimitExceeded) { log.WriteString(i18n.T("api.slackimport.slack_import.zip.file_too_large", map[string]any{"Filename": file.Name})) continue } @@ -179,7 +179,7 @@ func (si *SlackImporter) SlackImport(c request.CTX, fileData multipart.File, fil spl := strings.Split(file.Name, "/") if len(spl) == 2 && strings.HasSuffix(spl[1], ".json") { newposts, err := slackParsePosts(reader) - if errors.Is(err, utils.SizeLimitExceeded) { + if errors.Is(err, utils.ErrSizeLimitExceeded) { log.WriteString(i18n.T("api.slackimport.slack_import.zip.file_too_large", map[string]any{"Filename": file.Name})) continue } diff --git a/server/platform/services/slackimport/slackimport_test.go b/server/platform/services/slackimport/slackimport_test.go index d4e3f2a8a51..553b6b1a68e 100644 --- a/server/platform/services/slackimport/slackimport_test.go +++ b/server/platform/services/slackimport/slackimport_test.go @@ -14,7 +14,7 @@ import ( "github.com/mattermost/mattermost/server/public/model" "github.com/mattermost/mattermost/server/public/shared/mlog" - "github.com/mattermost/mattermost/server/v8/channels/app/request" + "github.com/mattermost/mattermost/server/public/shared/request" "github.com/mattermost/mattermost/server/v8/channels/store/storetest/mocks" ) @@ -339,8 +339,7 @@ func TestOldImportChannel(t *testing.T) { store := &mocks.Store{} config := &model.Config{} config.SetDefaults() - ctx := request.EmptyContext(nil) - ctx.SetLogger(mlog.CreateConsoleTestLogger(true, mlog.LvlDebug)) + ctx := request.EmptyContext(mlog.CreateConsoleTestLogger(t)) t.Run("No panic on direct channel", func(t *testing.T) { // ch := th.CreateDmChannel(u1) diff --git a/server/platform/services/telemetry/telemetry.go b/server/platform/services/telemetry/telemetry.go index ad5b7d0bebc..6369f97a0f7 100644 --- a/server/platform/services/telemetry/telemetry.go +++ b/server/platform/services/telemetry/telemetry.go @@ -419,8 +419,6 @@ func (ts *TelemetryService) trackConfig() { "enable_custom_emoji": *cfg.ServiceSettings.EnableCustomEmoji, "enable_emoji_picker": *cfg.ServiceSettings.EnableEmojiPicker, "enable_gif_picker": *cfg.ServiceSettings.EnableGifPicker, - "gfycat_api_key": isDefault(*cfg.ServiceSettings.GfycatAPIKey, model.ServiceSettingsDefaultGfycatAPIKey), - "gfycat_api_secret": isDefault(*cfg.ServiceSettings.GfycatAPISecret, model.ServiceSettingsDefaultGfycatAPISecret), "experimental_enable_authentication_transfer": *cfg.ServiceSettings.ExperimentalEnableAuthenticationTransfer, "enable_testing": cfg.ServiceSettings.EnableTesting, "enable_developer": *cfg.ServiceSettings.EnableDeveloper, @@ -829,13 +827,12 @@ func (ts *TelemetryService) trackConfig() { ts.SendTelemetry(TrackConfigDataRetention, map[string]any{ "enable_message_deletion": *cfg.DataRetentionSettings.EnableMessageDeletion, "enable_file_deletion": *cfg.DataRetentionSettings.EnableFileDeletion, - "enable_boards_deletion": *cfg.DataRetentionSettings.EnableBoardsDeletion, "message_retention_days": *cfg.DataRetentionSettings.MessageRetentionDays, "file_retention_days": *cfg.DataRetentionSettings.FileRetentionDays, - "boards_retention_days": *cfg.DataRetentionSettings.BoardsRetentionDays, "deletion_job_start_time": *cfg.DataRetentionSettings.DeletionJobStartTime, "batch_size": *cfg.DataRetentionSettings.BatchSize, "time_between_batches": *cfg.DataRetentionSettings.TimeBetweenBatchesMilliseconds, + "retention_ids_batch_size": *cfg.DataRetentionSettings.RetentionIdsBatchSize, "cleanup_jobs_threshold_days": *cfg.JobSettings.CleanupJobsThresholdDays, "cleanup_config_threshold_days": *cfg.JobSettings.CleanupConfigThresholdDays, }) @@ -857,6 +854,7 @@ func (ts *TelemetryService) trackConfig() { ts.SendTelemetry(TrackConfigDisplay, map[string]any{ "experimental_timezone": *cfg.DisplaySettings.ExperimentalTimezone, "isdefault_custom_url_schemes": len(cfg.DisplaySettings.CustomURLSchemes) != 0, + "isdefault_max_markdown_nodes": isDefault(*cfg.DisplaySettings.MaxMarkdownNodes, 0), }) ts.SendTelemetry(TrackConfigGuestAccounts, map[string]any{ diff --git a/server/platform/shared/filestore/filesstore.go b/server/platform/shared/filestore/filesstore.go index 5e9e5411e45..a26a9554e46 100644 --- a/server/platform/shared/filestore/filesstore.go +++ b/server/platform/shared/filestore/filesstore.go @@ -23,6 +23,7 @@ type ReadCloseSeeker interface { } type FileBackend interface { + DriverName() string TestConnection() error Reader(path string) (ReadCloseSeeker, error) diff --git a/server/platform/shared/filestore/filesstore_test.go b/server/platform/shared/filestore/filesstore_test.go index ddf5f6eb891..83bdd4df0c2 100644 --- a/server/platform/shared/filestore/filesstore_test.go +++ b/server/platform/shared/filestore/filesstore_test.go @@ -36,8 +36,7 @@ type FileBackendTestSuite struct { func TestLocalFileBackendTestSuite(t *testing.T) { // Setup a global logger to catch tests logging outside of app context // The global logger will be stomped by apps initializing but that's fine for testing. Ideally this won't happen. - logger := mlog.CreateConsoleTestLogger(true, mlog.LvlError) - defer logger.Shutdown() + logger := mlog.CreateConsoleTestLogger(t) mlog.InitGlobalLogger(logger) diff --git a/server/platform/shared/filestore/localstore.go b/server/platform/shared/filestore/localstore.go index d8c28d581c6..893dac58a27 100644 --- a/server/platform/shared/filestore/localstore.go +++ b/server/platform/shared/filestore/localstore.go @@ -68,6 +68,10 @@ func copyFile(src, dst string) (err error) { return } +func (b *LocalFileBackend) DriverName() string { + return driverLocal +} + func (b *LocalFileBackend) TestConnection() error { f := bytes.NewReader([]byte("testingwrite")) if _, err := writeFileLocally(f, filepath.Join(b.directory, TestFilePath)); err != nil { diff --git a/server/platform/shared/filestore/mocks/FileBackend.go b/server/platform/shared/filestore/mocks/FileBackend.go index 9a37de979f4..8d81a03d91c 100644 --- a/server/platform/shared/filestore/mocks/FileBackend.go +++ b/server/platform/shared/filestore/mocks/FileBackend.go @@ -57,6 +57,20 @@ func (_m *FileBackend) CopyFile(oldPath string, newPath string) error { return r0 } +// DriverName provides a mock function with given fields: +func (_m *FileBackend) DriverName() string { + ret := _m.Called() + + var r0 string + if rf, ok := ret.Get(0).(func() string); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(string) + } + + return r0 +} + // FileExists provides a mock function with given fields: path func (_m *FileBackend) FileExists(path string) (bool, error) { ret := _m.Called(path) diff --git a/server/platform/shared/filestore/s3store.go b/server/platform/shared/filestore/s3store.go index fed33701528..95008e1c1de 100644 --- a/server/platform/shared/filestore/s3store.go +++ b/server/platform/shared/filestore/s3store.go @@ -184,6 +184,10 @@ func (b *S3FileBackend) s3New(isCloud bool) (*s3.Client, error) { return s3Clnt, nil } +func (b *S3FileBackend) DriverName() string { + return driverS3 +} + func (b *S3FileBackend) TestConnection() error { exists := true var err error diff --git a/server/public/Makefile b/server/public/Makefile new file mode 100644 index 00000000000..85c96e86fcc --- /dev/null +++ b/server/public/Makefile @@ -0,0 +1,14 @@ +test-public: gotestsum + $(GOBIN)/gotestsum ./public/... -- $(GOFLAGS) + +## Generates mock golang interfaces for testing +mocks-public: + $(GO) install github.com/golang/mock/mockgen@v1.6.0 + $(GOBIN)/mockgen -destination public/pluginapi/experimental/panel/mocks/mock_panel.go -package mock_panel github.com/mattermost/mattermost/server/public/pluginapi/experimental/panel Panel + $(GOBIN)/mockgen -destination public/pluginapi/experimental/panel/mocks/mock_panelStore.go -package mock_panel github.com/mattermost/mattermost/server/public/pluginapi/experimental/panel Store + $(GOBIN)/mockgen -destination public/pluginapi/experimental/panel/mocks/mock_setting.go -package mock_panel github.com/mattermost/mattermost/server/public/pluginapi/experimental/panel/settings Setting + $(GOBIN)/mockgen -destination public/pluginapi/experimental/bot/mocks/mock_bot.go -package mock_bot github.com/mattermost/mattermost/server/public/pluginapi/experimental/bot Bot + $(GOBIN)/mockgen -destination public/pluginapi/experimental/bot/mocks/mock_logger.go -package mock_bot github.com/mattermost/mattermost/server/public/pluginapi/experimental/bot/logger Logger + $(GOBIN)/mockgen -destination public/pluginapi/experimental/bot/mocks/mock_poster.go -package mock_bot github.com/mattermost/mattermost/server/public/pluginapi/experimental/bot/poster Poster + $(GOBIN)/mockgen -destination public/pluginapi/experimental/oauther/mocks/mock_oauther.go -package mock_oauther github.com/mattermost/mattermost/server/public/pluginapi/experimental/oauther OAuther + $(GOBIN)/mockgen -destination public/pluginapi/experimental/bot/poster/mock_import/mock_postapi.go -package mock_import github.com/mattermost/mattermost/server/public/pluginapi/experimental/bot/poster PostAPI diff --git a/server/public/go.mod b/server/public/go.mod index dcf3bbca85c..3c9e32c478f 100644 --- a/server/public/go.mod +++ b/server/public/go.mod @@ -1,12 +1,14 @@ module github.com/mattermost/mattermost/server/public -go 1.19 +go 1.20 require ( - github.com/blang/semver v3.5.1+incompatible + github.com/blang/semver/v4 v4.0.0 github.com/dyatlov/go-opengraph/opengraph v0.0.0-20220524092352-606d7b1e5f8a github.com/francoispqt/gojay v1.2.13 github.com/go-sql-driver/mysql v1.7.1 + github.com/golang/mock v1.6.0 + github.com/gorilla/mux v1.8.0 github.com/gorilla/websocket v1.5.0 github.com/graph-gophers/graphql-go v1.5.1-0.20230110080634-edea822f558a github.com/hashicorp/go-hclog v1.5.0 @@ -15,18 +17,23 @@ require ( github.com/mattermost/go-i18n v1.11.1-0.20211013152124-5c415071e404 github.com/mattermost/ldap v0.0.0-20201202150706-ee0e6284187d github.com/mattermost/logr/v2 v2.0.16 + github.com/nicksnyder/go-i18n/v2 v2.0.3 github.com/pborman/uuid v1.2.1 github.com/pkg/errors v0.9.1 + github.com/rudderlabs/analytics-go v3.3.3+incompatible + github.com/sirupsen/logrus v1.9.3 github.com/stretchr/testify v1.8.4 github.com/tinylib/msgp v1.1.8 github.com/vmihailenco/msgpack/v5 v5.3.5 golang.org/x/crypto v0.10.0 + golang.org/x/oauth2 v0.7.0 golang.org/x/text v0.10.0 golang.org/x/tools v0.10.0 gopkg.in/yaml.v2 v2.4.0 ) require ( + github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/fatih/color v1.15.0 // indirect github.com/go-asn1-ber/asn1-ber v1.5.4 // indirect @@ -41,13 +48,20 @@ require ( github.com/pelletier/go-toml v1.9.5 // indirect github.com/philhofer/fwd v1.1.2 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/rogpeppe/go-internal v1.8.0 // indirect + github.com/segmentio/backo-go v1.0.1 // indirect github.com/stretchr/objx v0.5.0 // indirect + github.com/tidwall/gjson v1.14.3 // indirect + github.com/tidwall/match v1.1.1 // indirect + github.com/tidwall/pretty v1.2.0 // indirect github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect github.com/wiggin77/merror v1.0.5 // indirect github.com/wiggin77/srslog v1.0.1 // indirect + github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c // indirect golang.org/x/mod v0.11.0 // indirect golang.org/x/net v0.11.0 // indirect golang.org/x/sys v0.9.0 // indirect + google.golang.org/appengine v1.6.7 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20230629202037-9506855d4529 // indirect google.golang.org/grpc v1.56.1 // indirect google.golang.org/protobuf v1.31.0 // indirect diff --git a/server/public/go.sum b/server/public/go.sum index 9442becd87a..5c6104dd0f0 100644 --- a/server/public/go.sum +++ b/server/public/go.sum @@ -7,11 +7,15 @@ dmitri.shuralyov.com/html/belt v0.0.0-20180602232347-f7d459c86be0/go.mod h1:JLBr dmitri.shuralyov.com/service/change v0.0.0-20181023043359-a85b471d5412/go.mod h1:a1inKt/atXimZ4Mv927x+r7UpyzRUf4emIoiiSC2TN4= dmitri.shuralyov.com/state v0.0.0-20180228185332-28bcc343414c/go.mod h1:0PRwlb0D6DFvNNtx+9ybjezNCa8XF0xaYcETyp6rHWU= git.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg= +github.com/BurntSushi/toml v0.3.0/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= -github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ= -github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= +github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= +github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= +github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 h1:DDGfHa7BWjL4YnC6+E63dPcxHo2sUxDIu8g3QgEJdRY= +github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4= github.com/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625/go.mod h1:HYsPBTaaSFSlLx/70C2HPIMNZpVV8+vt/A+FMnYP11g= github.com/buger/jsonparser v0.0.0-20181115193947-bf1c66bbce23/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= @@ -46,6 +50,8 @@ github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfU github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E= 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.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= +github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= @@ -66,6 +72,8 @@ github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+ github.com/googleapis/gax-go v2.0.0+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY= github.com/googleapis/gax-go/v2 v2.0.3/go.mod h1:LLvjysVCY1JZeum8Z6l8qUty8fiNwE08qbEPm1M08qg= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= +github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/graph-gophers/graphql-go v1.5.1-0.20230110080634-edea822f558a h1:i0+Se9S+2zL5CBxJouqn2Ej6UQMwH1c57ZB6DVnqck4= @@ -119,6 +127,8 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJ github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86/go.mod h1:kHJEU3ofeGjhHklVoIGuVj85JJwZ6kWPaJwCIxgnFmo= github.com/neelance/sourcemap v0.0.0-20151028013722-8c68805598ab/go.mod h1:Qr6/a/Q4r9LP1IltGz7tA7iOK1WonHEYhu1HRBA7ZiM= +github.com/nicksnyder/go-i18n/v2 v2.0.3 h1:ks/JkQiOEhhuF6jpNvx+Wih1NIiXzUnZeZVnJuI8R8M= +github.com/nicksnyder/go-i18n/v2 v2.0.3/go.mod h1:oDab7q8XCYMRlcrBnaY/7B1eOectbvj6B1UPBT+p5jo= github.com/oklog/run v1.1.0 h1:GEenZ1cK0+q0+wsJew9qUg/DyD8k3JzYsZAi5gYi2mA= github.com/oklog/run v1.1.0/go.mod h1:sVPdnTZT1zYwAJeCMu2Th4T21pA3FPOQRfWjQlk7DVU= github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc= @@ -130,6 +140,7 @@ github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3v github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= github.com/philhofer/fwd v1.1.2 h1:bnDivRJ1EWPjUIRXV5KfORO897HTbpFAQddBdE8t7Gw= github.com/philhofer/fwd v1.1.2/go.mod h1:qkPdfjR2SIEbspLqpe1tO4n5yICnr2DY7mqEx2tUTP0= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= 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= @@ -139,9 +150,14 @@ github.com/prometheus/client_golang v0.8.0/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXP github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/common v0.0.0-20180801064454-c7de2306084e/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= github.com/prometheus/procfs v0.0.0-20180725123919-05ee40e3a273/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= -github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k= 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/rudderlabs/analytics-go v3.3.3+incompatible h1:OG0XlKoXfr539e2t1dXtTB+Gr89uFW+OUNQBVhHIIBY= +github.com/rudderlabs/analytics-go v3.3.3+incompatible/go.mod h1:LF8/ty9kUX4PTY3l5c97K3nZZaX5Hwsvt+NBaRL/f30= github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= +github.com/segmentio/backo-go v1.0.1 h1:68RQccglxZeyURy93ASB/2kc9QudzgIDexJ927N++y4= +github.com/segmentio/backo-go v1.0.1/go.mod h1:9/Rh6yILuLysoQnZ2oNooD2g7aBnvM7r/fNVxRNWfBc= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= github.com/shurcooL/component v0.0.0-20170202220835-f88ec8f54cc4/go.mod h1:XhFIlyj5a1fBNx5aJTbKoIq0mNaPvOagO+HjB3EtxrY= github.com/shurcooL/events v0.0.0-20181021180414-410e4ca65f48/go.mod h1:5u70Mqkb5O5cxEA8nxTsgrgLehJeAw6Oc4Ab1c/P1HM= @@ -165,6 +181,8 @@ github.com/shurcooL/reactions v0.0.0-20181006231557-f2e0b4ca5b82/go.mod h1:TCR1l github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/shurcooL/users v0.0.0-20180125191416-49c67e49c537/go.mod h1:QJTqeLYEDaXHZDBsXlPCDqdhQuJkuw4NOtaxYe3xii4= github.com/shurcooL/webdavfs v0.0.0-20170829043945-18c3829fa133/go.mod h1:hKmq5kWdCj2z2KEozexVbfEZIWiTjhE0+UjmZgPqehw= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d/go.mod h1:UdhH50NIW0fCiwBSr0co2m7BnFLdv4fQTgdqdJTHFeE= github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e/go.mod h1:HuIsMU8RRBOtsCgI77wP899iHVBQpCmg4ErYMZB+2IA= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -174,12 +192,19 @@ github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpE 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.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.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA= +github.com/tidwall/gjson v1.14.3 h1:9jvXn7olKEHU1S9vwoMGliaT8jq1vJ7IH/n9zD9Dnlw= +github.com/tidwall/gjson v1.14.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 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs= +github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/tinylib/msgp v1.1.8 h1:FCXC1xanKO4I8plpHGH2P7koL/RzZs12l/+r7vakfm0= github.com/tinylib/msgp v1.1.8/go.mod h1:qkpG+2ldGg4xRFmx+jfTvZPxfGFhi64BcnL9vkCm/Tw= github.com/viant/assertly v0.4.8/go.mod h1:aGifi++jvCrUaklKEKT0BU95igDNaqkvz+49uaYMPRU= @@ -192,6 +217,9 @@ github.com/wiggin77/merror v1.0.5 h1:P+lzicsn4vPMycAf2mFf7Zk6G9eco5N+jB1qJ2XW3ME github.com/wiggin77/merror v1.0.5/go.mod h1:H2ETSu7/bPE0Ymf4bEwdUoo73OOEkdClnoRisfw0Nm0= github.com/wiggin77/srslog v1.0.1 h1:gA2XjSMy3DrRdX9UqLuDtuVAAshb8bE1NhX1YK0Qe+8= github.com/wiggin77/srslog v1.0.1/go.mod h1:fehkyYDq1QfuYn60TDPu9YdY2bB85VUW2mvN1WynEls= +github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c h1:3lbZUMbMiGUW/LMkfsEABsc5zNT9+b1CvsJx47JzJ8g= +github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c/go.mod h1:UrdRz5enIKZ63MEE3IF9l2/ebyx59GyGgPi+tICQdmM= +github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.opencensus.io v0.18.0/go.mod h1:vKdFvxhtzZ9onBp9VKHK8z/sRpBMnKAsufL7wlDrCOA= go.opentelemetry.io/otel v1.6.3/go.mod h1:7BgNga5fNlF/iZjG06hM3yofffp0ofKCDwSXx1GC4dI= @@ -201,6 +229,8 @@ golang.org/x/build v0.0.0-20190111050920-041ab4dc3f9d/go.mod h1:OWs+y06UdEOHN4y+ golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/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-20190313024323-a1f597ede03a/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190506204251-e1dfcc566284/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.10.0 h1:LKqV2xt9+kDzSTfOhx4FrkEBcMrAgHSYgzywV9zcGmM= golang.org/x/crypto v0.10.0/go.mod h1:o4eNf7Ede1fv+hwOwZsTHl9EsPFO6q6ZvYR8vYfY45I= @@ -208,6 +238,7 @@ golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 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/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.11.0 h1:bUO06HqtnRcc/7l71XBe4WcqTZ+3AH1J59zWDDwLKgU= @@ -219,9 +250,14 @@ golang.org/x/net v0.0.0-20181029044818-c44066c5c816/go.mod h1:mL1N/T3taQHkDXs73r golang.org/x/net v0.0.0-20181106065722-10aee1819953/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 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-20190313220215-9f648a60d977/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/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-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20220520000938-2e3eb7b945c2/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= @@ -231,12 +267,15 @@ golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAG golang.org/x/oauth2 v0.0.0-20181017192945-9dcd33a902f4/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.7.0 h1:qe6s0zUXlPX80/dITx3440hWZ7GwMwgDDyrSGTPJG/g= +golang.org/x/oauth2 v0.7.0/go.mod h1:hPLQkd9LyjfXTiRohC/41GhcFqxisoUQ99sCUOHO9x4= golang.org/x/perf v0.0.0-20180704124530-6e6d33e29852/go.mod h1:JLpeXjPJfIyPr5TlbXLkXWLhP8nz10XfvxElABhCtcw= 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-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E= @@ -245,15 +284,20 @@ golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20181029174526-d69651ed3497/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-20190316082340-a2f829d7f35f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-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-20210927094055-39ccf1dd6fa6/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-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -265,6 +309,7 @@ golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuX golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA= 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.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= @@ -277,13 +322,17 @@ golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGm golang.org/x/tools v0.0.0-20181030000716-a0a13e073c7b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.4.0/go.mod h1:UE5sM2OK9E/d67R0ANs2xJizIymRP5gJU295PvKXxjQ= golang.org/x/tools v0.10.0 h1:tvDr/iQoUqNdohiYm0LmmKcBk+q86lb9EprIUFhHHGg= golang.org/x/tools v0.10.0/go.mod h1:UJwyiVBsOA2uwvK/e5OY3GTpDUJriEd+/YlqAwLPmyM= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/api v0.0.0-20180910000450-7ca32eb868bf/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= google.golang.org/api v0.0.0-20181030000543-1d582fd0359e/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= google.golang.org/api v0.1.0/go.mod h1:UGEZY7KEX120AnNLIHFMKIo4obdJhkp2tPbaPlQx13Y= @@ -291,6 +340,8 @@ google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9Ywl google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= +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-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20181029155118-b69ba1387ce2/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= diff --git a/server/public/model/bot.go b/server/public/model/bot.go index 4b8f84fca3b..5a5ce4970d4 100644 --- a/server/public/model/bot.go +++ b/server/public/model/bot.go @@ -213,8 +213,8 @@ func (l *BotList) Etag() string { // MakeBotNotFoundError creates the error returned when a bot does not exist, or when the user isn't allowed to query the bot. // The errors must the same in both cases to avoid leaking that a user is a bot. -func MakeBotNotFoundError(userId string) *AppError { - return NewAppError("SqlBotStore.Get", "store.sql_bot.get.missing.app_error", map[string]any{"user_id": userId}, "", http.StatusNotFound) +func MakeBotNotFoundError(where, userId string) *AppError { + return NewAppError(where, "store.sql_bot.get.missing.app_error", map[string]any{"user_id": userId}, "", http.StatusNotFound) } func IsBotDMChannel(channel *Channel, botUserID string) bool { diff --git a/server/public/model/bulk_export.go b/server/public/model/bulk_export.go index b18c32a3abb..3a32d3468f3 100644 --- a/server/public/model/bulk_export.go +++ b/server/public/model/bulk_export.go @@ -8,6 +8,7 @@ package model const ExportDataDir = "data" type BulkExportOpts struct { - IncludeAttachments bool - CreateArchive bool + IncludeAttachments bool + IncludeArchivedChannels bool + CreateArchive bool } diff --git a/server/public/model/channel.go b/server/public/model/channel.go index 7c8e979bec0..169d87760bf 100644 --- a/server/public/model/channel.go +++ b/server/public/model/channel.go @@ -443,3 +443,10 @@ func GetGroupNameFromUserIds(userIds []string) string { return hex.EncodeToString(h.Sum(nil)) } + +type GroupMessageConversionRequestBody struct { + ChannelID string `json:"channel_id"` + TeamID string `json:"team_id"` + Name string `json:"name"` + DisplayName string `json:"display_name"` +} diff --git a/server/public/model/client4.go b/server/public/model/client4.go index af991fde8c0..65626ca3fb7 100644 --- a/server/public/model/client4.go +++ b/server/public/model/client4.go @@ -467,10 +467,6 @@ func (c *Client4) oAuthAppRoute(appId string) string { return fmt.Sprintf("/oauth/apps/%v", appId) } -func (c *Client4) openGraphRoute() string { - return "/opengraph" -} - func (c *Client4) jobsRoute() string { return "/jobs" } @@ -3540,6 +3536,27 @@ func (c *Client4) ViewChannel(ctx context.Context, userId string, view *ChannelV return ch, BuildResponse(r), nil } +// ReadMultipleChannels performs a view action on several channels at the same time for a user. +func (c *Client4) ReadMultipleChannels(ctx context.Context, userId string, channelIds []string) (*ChannelViewResponse, *Response, error) { + url := fmt.Sprintf(c.channelsRoute()+"/members/%v/mark_read", userId) + buf, err := json.Marshal(channelIds) + if err != nil { + return nil, nil, NewAppError("ReadMultipleChannels", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err) + } + r, err := c.DoAPIPostBytes(ctx, url, buf) + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + + var ch *ChannelViewResponse + err = json.NewDecoder(r.Body).Decode(&ch) + if err != nil { + return nil, BuildResponse(r), NewAppError("ReadMultipleChannels", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err) + } + return ch, BuildResponse(r), nil +} + // GetChannelUnread will return a ChannelUnread object that contains the number of // unread messages and mentions for a user. func (c *Client4) GetChannelUnread(ctx context.Context, channelId, userId string) (*ChannelUnread, *Response, error) { @@ -5493,7 +5510,7 @@ func (c *Client4) GetGroupsAssociatedToChannelsByTeam(ctx context.Context, teamI // GetGroups retrieves Mattermost Groups func (c *Client4) GetGroups(ctx context.Context, opts GroupSearchOpts) ([]*Group, *Response, error) { path := fmt.Sprintf( - "%s?include_member_count=%v¬_associated_to_team=%v¬_associated_to_channel=%v&filter_allow_reference=%v&q=%v&filter_parent_team_permitted=%v&group_source=%v&include_channel_member_count=%v&include_timezones=%v", + "%s?include_member_count=%v¬_associated_to_team=%v¬_associated_to_channel=%v&filter_allow_reference=%v&q=%v&filter_parent_team_permitted=%v&group_source=%v&include_channel_member_count=%v&include_timezones=%v&include_archived=%v&filter_archived=%v", c.groupsRoute(), opts.IncludeMemberCount, opts.NotAssociatedToTeam, @@ -5504,6 +5521,8 @@ func (c *Client4) GetGroups(ctx context.Context, opts GroupSearchOpts) ([]*Group opts.Source, opts.IncludeChannelMemberCount, opts.IncludeTimezones, + opts.IncludeArchived, + opts.FilterArchived, ) if opts.Since > 0 { path = fmt.Sprintf("%s&since=%v", path, opts.Since) @@ -6758,21 +6777,6 @@ func (c *Client4) GetSupportedTimezone(ctx context.Context) ([]string, *Response return timezones, BuildResponse(r), nil } -// Open Graph Metadata Section - -// OpenGraph return the open graph metadata for a particular url if the site have the metadata. -func (c *Client4) OpenGraph(ctx context.Context, url string) (map[string]string, *Response, error) { - requestBody := make(map[string]string) - requestBody["url"] = url - - r, err := c.DoAPIPost(ctx, c.openGraphRoute(), MapToJSON(requestBody)) - if err != nil { - return nil, BuildResponse(r), err - } - defer closeBody(r) - return MapFromJSON(r.Body), BuildResponse(r), nil -} - // Jobs Section // GetJob gets a single job. diff --git a/server/public/model/cloud.go b/server/public/model/cloud.go index 97b27bcd322..b81bf91f038 100644 --- a/server/public/model/cloud.go +++ b/server/public/model/cloud.go @@ -266,10 +266,11 @@ type CloudWorkspaceOwner struct { } type SubscriptionChange struct { - ProductID string `json:"product_id"` - Seats int `json:"seats"` - Feedback *Feedback `json:"downgrade_feedback"` - ShippingAddress *Address `json:"shipping_address"` + ProductID string `json:"product_id"` + Seats int `json:"seats"` + Feedback *Feedback `json:"downgrade_feedback"` + ShippingAddress *Address `json:"shipping_address"` + Customer *CloudCustomerInfo `json:"customer"` } type FilesLimits struct { diff --git a/server/public/model/command_autocomplete.go b/server/public/model/command_autocomplete.go index 63092a59eb7..d1fd08243db 100644 --- a/server/public/model/command_autocomplete.go +++ b/server/public/model/command_autocomplete.go @@ -11,6 +11,8 @@ import ( "strings" "github.com/pkg/errors" + + "github.com/mattermost/mattermost/server/public/utils" ) // AutocompleteArgType describes autocomplete argument type @@ -234,7 +236,7 @@ func (ad *AutocompleteData) IsValid() error { return errors.New("Command should be lowercase") } roles := []string{SystemAdminRoleId, SystemUserRoleId, ""} - if stringNotInSlice(ad.RoleID, roles) { + if !utils.Contains(roles, ad.RoleID) { return errors.New("Wrong role in the autocomplete data") } if len(ad.Arguments) > 0 && len(ad.SubCommands) > 0 { @@ -399,12 +401,3 @@ func (a *AutocompleteArg) UnmarshalJSON(b []byte) error { } return nil } - -func stringNotInSlice(a string, slice []string) bool { - for _, b := range slice { - if b == a { - return false - } - } - return true -} diff --git a/server/public/model/config.go b/server/public/model/config.go index b5d17dbe713..0a799a356c7 100644 --- a/server/public/model/config.go +++ b/server/public/model/config.go @@ -109,8 +109,7 @@ const ( ServiceSettingsDefaultMaxLoginAttempts = 10 ServiceSettingsDefaultAllowCorsFrom = "" ServiceSettingsDefaultListenAndAddress = ":8065" - ServiceSettingsDefaultGfycatAPIKey = "2_KtH_W5" - ServiceSettingsDefaultGfycatAPISecret = "3wLVZPiswc3DnaiaFoLkDvB4X0IV6CpMkj4tf2inJRsBY6-FnkT08zGmppWFgeof" + ServiceSettingsDefaultGiphySdkKeyTest = "s0glxvzVg9azvPipKxcPLpXV0q1x1fVP" ServiceSettingsDefaultDeveloperFlags = "" TeamSettingsDefaultSiteName = "Mattermost" @@ -133,7 +132,7 @@ const ( SupportSettingsDefaultTermsOfServiceLink = "https://mattermost.com/pl/terms-of-use/" SupportSettingsDefaultPrivacyPolicyLink = "https://mattermost.com/pl/privacy-policy/" - SupportSettingsDefaultAboutLink = "https://mattermost.com/pl/about-mattermomst" + SupportSettingsDefaultAboutLink = "https://mattermost.com/pl/about-mattermost" SupportSettingsDefaultHelpLink = "https://mattermost.com/pl/help/" SupportSettingsDefaultReportAProblemLink = "https://mattermost.com/pl/report-a-bug" SupportSettingsDefaultSupportEmail = "" @@ -211,6 +210,7 @@ const ( DataRetentionSettingsDefaultDeletionJobStartTime = "02:00" DataRetentionSettingsDefaultBatchSize = 3000 DataRetentionSettingsDefaultTimeBetweenBatchesMilliseconds = 100 + DataRetentionSettingsDefaultRetentionIdsBatchSize = 100 PluginSettingsDefaultDirectory = "./plugins" PluginSettingsDefaultClientDirectory = "./client/plugins" @@ -346,8 +346,7 @@ type ServiceSettings struct { WebsocketPort *int `access:"write_restrictable,cloud_restrictable"` // telemetry: none WebserverMode *string `access:"environment_web_server,write_restrictable,cloud_restrictable"` EnableGifPicker *bool `access:"integrations_gif"` - GfycatAPIKey *string `access:"integrations_gif"` - GfycatAPISecret *string `access:"integrations_gif"` + GiphySdkKey *string `access:"integrations_gif"` EnableCustomEmoji *bool `access:"site_emoji"` EnableEmojiPicker *bool `access:"site_emoji"` PostEditTimeLimit *int `access:"user_management_permissions"` @@ -397,6 +396,8 @@ type ServiceSettings struct { AllowSyncedDrafts *bool `access:"site_posts"` } +var MattermostGiphySdkKey string + func (s *ServiceSettings) SetDefaults(isUpdate bool) { if s.EnableEmailInvitations == nil { // If the site URL is also not present then assume this is a clean install @@ -732,12 +733,8 @@ func (s *ServiceSettings) SetDefaults(isUpdate bool) { s.EnableGifPicker = NewBool(true) } - if s.GfycatAPIKey == nil || *s.GfycatAPIKey == "" { - s.GfycatAPIKey = NewString(ServiceSettingsDefaultGfycatAPIKey) - } - - if s.GfycatAPISecret == nil || *s.GfycatAPISecret == "" { - s.GfycatAPISecret = NewString(ServiceSettingsDefaultGfycatAPISecret) + if s.GiphySdkKey == nil || *s.GiphySdkKey == "" { + s.GiphySdkKey = NewString("") } if s.ExperimentalEnableAuthenticationTransfer == nil { @@ -1279,6 +1276,7 @@ type LogSettings struct { EnableSentry *bool `access:"environment_logging,write_restrictable,cloud_restrictable"` // telemetry: none AdvancedLoggingJSON json.RawMessage `access:"environment_logging,write_restrictable,cloud_restrictable"` AdvancedLoggingConfig *string `access:"environment_logging,write_restrictable,cloud_restrictable"` // Deprecated: use `AdvancedLoggingJSON` + MaxFieldSize *int `access:"environment_logging,write_restrictable,cloud_restrictable"` } func NewLogSettings() *LogSettings { @@ -1343,6 +1341,10 @@ func (s *LogSettings) SetDefaults() { if s.AdvancedLoggingConfig == nil { s.AdvancedLoggingConfig = NewString("") } + + if s.MaxFieldSize == nil { + s.MaxFieldSize = NewInt(2048) + } } // GetAdvancedLoggingConfig returns the advanced logging config as a []byte. @@ -2117,18 +2119,19 @@ func (s *ThemeSettings) SetDefaults() { } type TeamSettings struct { - SiteName *string `access:"site_customization"` - MaxUsersPerTeam *int `access:"site_users_and_teams"` - EnableUserCreation *bool `access:"authentication_signup"` - EnableOpenServer *bool `access:"authentication_signup"` - EnableUserDeactivation *bool `access:"experimental_features"` - RestrictCreationToDomains *string `access:"authentication_signup"` // telemetry: none - EnableCustomUserStatuses *bool `access:"site_users_and_teams"` - EnableCustomBrand *bool `access:"site_customization"` - CustomBrandText *string `access:"site_customization"` - CustomDescriptionText *string `access:"site_customization"` - RestrictDirectMessage *string `access:"site_users_and_teams"` - EnableLastActiveTime *bool `access:"site_users_and_teams"` + SiteName *string `access:"site_customization"` + MaxUsersPerTeam *int `access:"site_users_and_teams"` + EnableJoinLeaveMessageByDefault *bool `access:"site_users_and_teams"` + EnableUserCreation *bool `access:"authentication_signup"` + EnableOpenServer *bool `access:"authentication_signup"` + EnableUserDeactivation *bool `access:"experimental_features"` + RestrictCreationToDomains *string `access:"authentication_signup"` // telemetry: none + EnableCustomUserStatuses *bool `access:"site_users_and_teams"` + EnableCustomBrand *bool `access:"site_customization"` + CustomBrandText *string `access:"site_customization"` + CustomDescriptionText *string `access:"site_customization"` + RestrictDirectMessage *string `access:"site_users_and_teams"` + EnableLastActiveTime *bool `access:"site_users_and_teams"` // In seconds. UserStatusAwayTimeout *int64 `access:"experimental_features"` MaxChannelsPerTeam *int64 `access:"site_users_and_teams"` @@ -2152,6 +2155,10 @@ func (s *TeamSettings) SetDefaults() { s.MaxUsersPerTeam = NewInt(TeamSettingsDefaultMaxUsersPerTeam) } + if s.EnableJoinLeaveMessageByDefault == nil { + s.EnableJoinLeaveMessageByDefault = NewBool(true) + } + if s.EnableUserCreation == nil { s.EnableUserCreation = NewBool(true) } @@ -2867,6 +2874,7 @@ type DataRetentionSettings struct { DeletionJobStartTime *string `access:"compliance_data_retention_policy"` BatchSize *int `access:"compliance_data_retention_policy"` TimeBetweenBatchesMilliseconds *int `access:"compliance_data_retention_policy"` + RetentionIdsBatchSize *int `access:"compliance_data_retention_policy"` } func (s *DataRetentionSettings) SetDefaults() { @@ -2905,6 +2913,9 @@ func (s *DataRetentionSettings) SetDefaults() { if s.TimeBetweenBatchesMilliseconds == nil { s.TimeBetweenBatchesMilliseconds = NewInt(DataRetentionSettingsDefaultTimeBetweenBatchesMilliseconds) } + if s.RetentionIdsBatchSize == nil { + s.RetentionIdsBatchSize = NewInt(DataRetentionSettingsDefaultRetentionIdsBatchSize) + } } type JobSettings struct { @@ -3136,7 +3147,8 @@ func (s *MessageExportSettings) SetDefaults() { } type DisplaySettings struct { - CustomURLSchemes []string `access:"site_customization"` + CustomURLSchemes []string `access:"site_posts"` + MaxMarkdownNodes *int `access:"site_posts"` ExperimentalTimezone *bool `access:"experimental_features"` } @@ -3146,6 +3158,10 @@ func (s *DisplaySettings) SetDefaults() { s.CustomURLSchemes = customURLSchemes } + if s.MaxMarkdownNodes == nil { + s.MaxMarkdownNodes = NewInt(0) + } + if s.ExperimentalTimezone == nil { s.ExperimentalTimezone = NewBool(true) } @@ -3489,6 +3505,10 @@ func (o *Config) IsValid() *AppError { return appErr } + if appErr := o.ExperimentalSettings.isValid(); appErr != nil { + return appErr + } + if appErr := o.SqlSettings.isValid(); appErr != nil { return appErr } @@ -3564,6 +3584,10 @@ func (s *TeamSettings) isValid() *AppError { return NewAppError("Config.IsValid", "model.config.is_valid.max_channels.app_error", nil, "", http.StatusBadRequest) } + if *s.UserStatusAwayTimeout <= 0 { + return NewAppError("Config.IsValid", "model.config.is_valid.user_status_away_timeout.app_error", nil, "", http.StatusBadRequest) + } + if *s.MaxNotificationsPerChannel <= 0 { return NewAppError("Config.IsValid", "model.config.is_valid.max_notify_per_channel.app_error", nil, "", http.StatusBadRequest) } @@ -3583,6 +3607,14 @@ func (s *TeamSettings) isValid() *AppError { return nil } +func (s *ExperimentalSettings) isValid() *AppError { + if *s.LinkMetadataTimeoutMilliseconds <= 0 { + return NewAppError("Config.IsValid", "model.config.is_valid.link_metadata_timeout.app_error", nil, "", http.StatusBadRequest) + } + + return nil +} + func (s *SqlSettings) isValid() *AppError { if *s.AtRestEncryptKey != "" && len(*s.AtRestEncryptKey) < 32 { return NewAppError("Config.IsValid", "model.config.is_valid.encrypt_sql.app_error", nil, "", http.StatusBadRequest) @@ -4168,10 +4200,6 @@ func (o *Config) Sanitize() { *o.MessageExportSettings.GlobalRelaySettings.SMTPPassword = FakeSetting } - if o.ServiceSettings.GfycatAPISecret != nil && *o.ServiceSettings.GfycatAPISecret != "" { - *o.ServiceSettings.GfycatAPISecret = FakeSetting - } - if o.ServiceSettings.SplitKey != nil { *o.ServiceSettings.SplitKey = FakeSetting } diff --git a/server/public/model/config_test.go b/server/public/model/config_test.go index 7477fbb5ef8..20497d13498 100644 --- a/server/public/model/config_test.go +++ b/server/public/model/config_test.go @@ -317,38 +317,13 @@ func TestConfigDefaultNPSPluginState(t *testing.T) { } func TestConfigDefaultChannelExportPluginState(t *testing.T) { - t.Run("should enable ChannelExport plugin by default on enterprise-ready builds", func(t *testing.T) { + t.Run("should not enable ChannelExport plugin by default", func(t *testing.T) { BuildEnterpriseReady = "true" c1 := Config{} c1.SetDefaults() - assert.True(t, c1.PluginSettings.PluginStates["com.mattermost.plugin-channel-export"].Enable) - }) - - t.Run("should not enable ChannelExport plugin by default on non-enterprise-ready builds", func(t *testing.T) { - BuildEnterpriseReady = "" - c1 := Config{} - c1.SetDefaults() - assert.Nil(t, c1.PluginSettings.PluginStates["com.mattermost.plugin-channel-export"]) }) - - t.Run("should not re-enable ChannelExport plugin after it has been disabled", func(t *testing.T) { - BuildEnterpriseReady = "" - c1 := Config{ - PluginSettings: PluginSettings{ - PluginStates: map[string]*PluginState{ - "com.mattermost.plugin-channel-export": { - Enable: false, - }, - }, - }, - } - - c1.SetDefaults() - - assert.False(t, c1.PluginSettings.PluginStates["com.mattermost.plugin-channel-export"].Enable) - }) } func TestTeamSettingsIsValidSiteNameEmpty(t *testing.T) { @@ -360,6 +335,14 @@ func TestTeamSettingsIsValidSiteNameEmpty(t *testing.T) { require.Nil(t, c1.TeamSettings.isValid()) } +func TestTeamSettingsDefaultJoinLeaveMessage(t *testing.T) { + c1 := Config{} + c1.SetDefaults() + + // should default to true + require.Equal(t, NewBool(true), c1.TeamSettings.EnableJoinLeaveMessageByDefault) +} + func TestMessageExportSettingsIsValidEnableExportNotSet(t *testing.T) { mes := &MessageExportSettings{} diff --git a/server/public/model/data_retention_policy.go b/server/public/model/data_retention_policy.go index 1e2817c28ac..8d48d0dca56 100644 --- a/server/public/model/data_retention_policy.go +++ b/server/public/model/data_retention_policy.go @@ -6,10 +6,8 @@ package model type GlobalRetentionPolicy struct { MessageDeletionEnabled bool `json:"message_deletion_enabled"` FileDeletionEnabled bool `json:"file_deletion_enabled"` - BoardsDeletionEnabled bool `json:"boards_deletion_enabled"` MessageRetentionCutoff int64 `json:"message_retention_cutoff"` FileRetentionCutoff int64 `json:"file_retention_cutoff"` - BoardsRetentionCutoff int64 `json:"boards_retention_cutoff"` } type RetentionPolicy struct { @@ -86,3 +84,15 @@ type RetentionPolicyCursor struct { TeamPoliciesDone bool GlobalPoliciesDone bool } + +type RetentionIdsForDeletion struct { + Id string + TableName string + Ids []string +} + +func (r *RetentionIdsForDeletion) PreSave() { + if r.Id == "" { + r.Id = NewId() + } +} diff --git a/server/public/model/feature_flags.go b/server/public/model/feature_flags.go index a820acd2891..3a2a470f629 100644 --- a/server/public/model/feature_flags.go +++ b/server/public/model/feature_flags.go @@ -27,9 +27,6 @@ type FeatureFlags struct { // CallsEnabled controls whether or not the Calls plugin should be enabled CallsEnabled bool - // Enable DataRetention for Boards - BoardsDataRetention bool - NormalizeLdapDNs bool // Enable GraphQL feature @@ -40,8 +37,6 @@ type FeatureFlags struct { // Enable WYSIWYG text editor WysiwygEditor bool - PeopleProduct bool - OnboardingTourTips bool DeprecateCloudFree bool @@ -50,7 +45,7 @@ type FeatureFlags struct { EnableExportDirectDownload bool - DataRetentionConcurrencyEnabled bool + StreamlinedMarketplace bool } func (f *FeatureFlags) SetDefaults() { @@ -58,17 +53,15 @@ func (f *FeatureFlags) SetDefaults() { f.TestBoolFeature = false f.EnableRemoteClusterService = false f.AppsEnabled = true - f.BoardsDataRetention = false f.NormalizeLdapDNs = false f.GraphQL = false f.CallsEnabled = true - f.PeopleProduct = false f.DeprecateCloudFree = false f.WysiwygEditor = false f.OnboardingTourTips = true f.CloudReverseTrial = false f.EnableExportDirectDownload = false - f.DataRetentionConcurrencyEnabled = true + f.StreamlinedMarketplace = true } // ToMap returns the feature flags as a map[string]string diff --git a/server/public/model/group.go b/server/public/model/group.go index 6a56b52e8a0..50d7d93ec4e 100644 --- a/server/public/model/group.go +++ b/server/public/model/group.go @@ -45,6 +45,7 @@ type Group struct { AllowReference bool `json:"allow_reference"` ChannelMemberCount *int `db:"-" json:"channel_member_count,omitempty"` ChannelMemberTimezonesCount *int `db:"-" json:"channel_member_timezones_count,omitempty"` + MemberIDs []string `db:"-" json:"member_ids"` } func (group *Group) Auditable() map[string]interface{} { @@ -133,10 +134,18 @@ type GroupSearchOpts struct { IncludeChannelMemberCount string IncludeTimezones bool + IncludeMemberIDs bool + + // Include archived groups + IncludeArchived bool + + // Only return archived groups + FilterArchived bool } type GetGroupOpts struct { IncludeMemberCount bool + IncludeMemberIDs bool } type PageOpts struct { diff --git a/server/public/model/job.go b/server/public/model/job.go index 5b2dea6731e..ddf6ddfd66d 100644 --- a/server/public/model/job.go +++ b/server/public/model/job.go @@ -6,6 +6,8 @@ package model import ( "net/http" "time" + + "github.com/mattermost/mattermost/server/public/shared/mlog" ) const ( @@ -36,6 +38,7 @@ const ( JobTypeInstallPluginNotifyAdmin = "install_plugin_notify_admin" JobTypeHostedPurchaseScreening = "hosted_purchase_screening" JobTypeS3PathMigration = "s3_path_migration" + JobTypeCleanupDesktopTokens = "cleanup_desktop_tokens" JobStatusPending = "pending" JobStatusInProgress = "in_progress" @@ -66,6 +69,7 @@ var AllJobTypes = [...]string{ JobTypeExtractContent, JobTypeLastAccessiblePost, JobTypeLastAccessibleFile, + JobTypeCleanupDesktopTokens, } type Job struct { @@ -78,6 +82,8 @@ type Job struct { Status string `json:"status"` Progress int64 `json:"progress"` Data StringMap `json:"data"` + + Logger *mlog.Logger `json:"-"` } func (j *Job) Auditable() map[string]interface{} { @@ -118,15 +124,23 @@ func (j *Job) IsValid() *AppError { return nil } +// InitLogger attaches an annotated logger to a Job. +// It should always be called after creating a new Job to ensure `Job.Logger` it set. +func (j *Job) InitLogger(logger mlog.LoggerIFace) { + j.Logger = logger.With( + mlog.String("job_id", j.Id), + mlog.String("job_type", j.Type), + mlog.String("create_at", time.UnixMilli(j.CreateAt).String()), + ) +} + +func (j *Job) LogClone() any { + return j.Auditable() +} + type Worker interface { Run() Stop() JobChannel() chan<- Job IsEnabled(cfg *Config) bool } - -type Scheduler interface { - Enabled(cfg *Config) bool - NextScheduleTime(cfg *Config, now time.Time, pendingJobs bool, lastSuccessfulJob *Job) *time.Time - ScheduleJob(cfg *Config, pendingJobs bool, lastSuccessfulJob *Job) (*Job, *AppError) -} diff --git a/server/public/model/manifest.go b/server/public/model/manifest.go index 66eb218e3b7..415a7e80025 100644 --- a/server/public/model/manifest.go +++ b/server/public/model/manifest.go @@ -11,7 +11,7 @@ import ( "path/filepath" "strings" - "github.com/blang/semver" + "github.com/blang/semver/v4" "github.com/pkg/errors" "gopkg.in/yaml.v2" ) @@ -188,11 +188,6 @@ type Manifest struct { // Plugins can store any kind of data in Props to allow other plugins to use it. Props map[string]any `json:"props,omitempty" yaml:"props,omitempty"` - - // RequiredConfig defines any required server configuration fields for the plugin to function properly. - // - // Use the pluginapi.Configuration.CheckRequiredServerConfiguration method to enforce this. - RequiredConfig *Config `json:"required_configuration,omitempty" yaml:"required_configuration,omitempty"` } type ManifestServer struct { diff --git a/server/public/model/post.go b/server/public/model/post.go index a24798f8864..fe0d4d6fb17 100644 --- a/server/public/model/post.go +++ b/server/public/model/post.go @@ -45,6 +45,7 @@ const ( PostTypeChannelRestored = "system_channel_restored" PostTypeEphemeral = "system_ephemeral" PostTypeChangeChannelPrivacy = "system_change_chan_privacy" + PostTypeGMConvertedToChannel = "system_gm_to_channel" PostTypeAddBotTeamsChannels = "add_bot_teams_channels" PostTypeSystemWarnMetricStatus = "warn_metric_status" PostTypeMe = "me" @@ -62,15 +63,18 @@ const ( PropsAddChannelMember = "add_channel_member" - PostPropsAddedUserId = "addedUserId" - PostPropsDeleteBy = "deleteBy" - PostPropsOverrideIconURL = "override_icon_url" - PostPropsOverrideIconEmoji = "override_icon_emoji" - + PostPropsAddedUserId = "addedUserId" + PostPropsDeleteBy = "deleteBy" + PostPropsOverrideIconURL = "override_icon_url" + PostPropsOverrideIconEmoji = "override_icon_emoji" + PostPropsOverrideUsername = "override_username" + PostPropsFromWebhook = "from_webhook" + PostPropsFromBot = "from_bot" + PostPropsFromOAuthApp = "from_oauth_app" + PostPropsWebhookDisplayName = "webhook_display_name" PostPropsMentionHighlightDisabled = "mentionHighlightDisabled" PostPropsGroupHighlightDisabled = "disable_group_highlight" - - PostPropsPreviewedPost = "previewed_post" + PostPropsPreviewedPost = "previewed_post" PostPriorityUrgent = "urgent" PostPropsRequestedAck = "requested_ack" @@ -428,7 +432,8 @@ func (o *Post) IsValid(maxPostSize int) *AppError { PostTypeAddBotTeamsChannels, PostTypeSystemWarnMetricStatus, PostTypeReminder, - PostTypeMe: + PostTypeMe, + PostTypeGMConvertedToChannel: default: if !strings.HasPrefix(o.Type, PostCustomTypePrefix) { return NewAppError("Post.IsValid", "model.post.is_valid.type.app_error", nil, "id="+o.Type, http.StatusBadRequest) @@ -468,6 +473,39 @@ func (o *Post) SanitizeProps() { } } +func (o *Post) ContainsIntegrationsReservedProps() []string { + return containsIntegrationsReservedProps(o.GetProps()) +} + +func (o *PostPatch) ContainsIntegrationsReservedProps() []string { + if o == nil || o.Props == nil { + return nil + } + return containsIntegrationsReservedProps(*o.Props) +} + +func containsIntegrationsReservedProps(props StringInterface) []string { + foundProps := []string{} + + if props != nil { + reservedProps := []string{ + PostPropsFromWebhook, + PostPropsOverrideUsername, + PostPropsWebhookDisplayName, + PostPropsOverrideIconURL, + PostPropsOverrideIconEmoji, + } + + for _, key := range reservedProps { + if _, ok := props[key]; ok { + foundProps = append(foundProps, key) + } + } + } + + return foundProps +} + func (o *Post) PreSave() { if o.Id == "" { o.Id = NewId() diff --git a/server/public/model/post_test.go b/server/public/model/post_test.go index 747ceb19fd7..1e47c52bd57 100644 --- a/server/public/model/post_test.go +++ b/server/public/model/post_test.go @@ -142,6 +142,41 @@ func TestPostSanitizeProps(t *testing.T) { require.NotNil(t, post3.GetProp("attachments")) } +func TestPost_ContainsIntegrationsReservedProps(t *testing.T) { + post1 := &Post{ + Message: "test", + } + keys1 := post1.ContainsIntegrationsReservedProps() + require.Len(t, keys1, 0) + + post2 := &Post{ + Message: "test", + Props: StringInterface{ + "from_webhook": "true", + "webhook_display_name": "overridden_display_name", + "override_username": "overridden_username", + "override_icon_url": "a-custom-url", + "override_icon_emoji": ":custom_emoji_name:", + }, + } + keys2 := post2.ContainsIntegrationsReservedProps() + require.Len(t, keys2, 5) +} + +func TestPostPatch_ContainsIntegrationsReservedProps(t *testing.T) { + postPatch1 := &PostPatch{ + Props: &StringInterface{ + "from_webhook": "true", + }, + } + keys1 := postPatch1.ContainsIntegrationsReservedProps() + require.Len(t, keys1, 1) + + postPatch2 := &PostPatch{} + keys2 := postPatch2.ContainsIntegrationsReservedProps() + require.Len(t, keys2, 0) +} + func TestPost_AttachmentsEqual(t *testing.T) { post1 := &Post{} post2 := &Post{} diff --git a/server/public/model/preference.go b/server/public/model/preference.go index a73e19dad69..bd54a88f667 100644 --- a/server/public/model/preference.go +++ b/server/public/model/preference.go @@ -31,6 +31,8 @@ const ( PreferenceNameUseMilitaryTime = "use_military_time" PreferenceRecommendedNextSteps = "recommended_next_steps" + PreferenceCategorySystemNotice = "system_notice" + PreferenceCategoryTheme = "theme" // the name for theme props is the team id diff --git a/server/public/model/push_notification.go b/server/public/model/push_notification.go index b2e6f947703..3151430920d 100644 --- a/server/public/model/push_notification.go +++ b/server/public/model/push_notification.go @@ -44,29 +44,31 @@ type PushNotificationAck struct { } type PushNotification struct { - AckId string `json:"ack_id"` - Platform string `json:"platform"` - ServerId string `json:"server_id"` - DeviceId string `json:"device_id"` - PostId string `json:"post_id"` - Category string `json:"category,omitempty"` - Sound string `json:"sound,omitempty"` - Message string `json:"message,omitempty"` - Badge int `json:"badge,omitempty"` - ContentAvailable int `json:"cont_ava,omitempty"` - TeamId string `json:"team_id,omitempty"` - ChannelId string `json:"channel_id,omitempty"` - RootId string `json:"root_id,omitempty"` - ChannelName string `json:"channel_name,omitempty"` - Type string `json:"type,omitempty"` - SenderId string `json:"sender_id,omitempty"` - SenderName string `json:"sender_name,omitempty"` - OverrideUsername string `json:"override_username,omitempty"` - OverrideIconURL string `json:"override_icon_url,omitempty"` - FromWebhook string `json:"from_webhook,omitempty"` - Version string `json:"version,omitempty"` - IsCRTEnabled bool `json:"is_crt_enabled"` - IsIdLoaded bool `json:"is_id_loaded"` + AckId string `json:"ack_id"` + Platform string `json:"platform"` + ServerId string `json:"server_id"` + DeviceId string `json:"device_id"` + PostId string `json:"post_id"` + Category string `json:"category,omitempty"` + Sound string `json:"sound,omitempty"` + Message string `json:"message,omitempty"` + Badge int `json:"badge,omitempty"` + ContentAvailable int `json:"cont_ava,omitempty"` + TeamId string `json:"team_id,omitempty"` + ChannelId string `json:"channel_id,omitempty"` + RootId string `json:"root_id,omitempty"` + ChannelName string `json:"channel_name,omitempty"` + Type string `json:"type,omitempty"` + SenderId string `json:"sender_id,omitempty"` + SenderName string `json:"sender_name,omitempty"` + OverrideUsername string `json:"override_username,omitempty"` + OverrideIconURL string `json:"override_icon_url,omitempty"` + FromWebhook string `json:"from_webhook,omitempty"` + Version string `json:"version,omitempty"` + IsCRTEnabled bool `json:"is_crt_enabled"` + IsIdLoaded bool `json:"is_id_loaded"` + PostType string `json:"-"` + ChannelType ChannelType `json:"-"` } func (pn *PushNotification) DeepCopy() *PushNotification { diff --git a/server/public/model/service_environment_test.go b/server/public/model/service_environment_test.go index 794bc7f4ff6..bf6503ca10a 100644 --- a/server/public/model/service_environment_test.go +++ b/server/public/model/service_environment_test.go @@ -16,13 +16,13 @@ import ( // semantics at the unit test level. Validating the default, enterprise service environment is left // to smoketests before releasing. func TestGetServiceEnvironment(t *testing.T) { - t.Run("no env defaults to test (without production tag)", func(t *testing.T) { - require.Equal(t, model.ServiceEnvironmentTest, model.GetServiceEnvironment()) + t.Run("no env defaults to dev (without production tag)", func(t *testing.T) { + require.Equal(t, model.ServiceEnvironmentDev, model.GetServiceEnvironment()) }) - t.Run("empty string defaults to test (without production tag)", func(t *testing.T) { + t.Run("empty string defaults to dev (without production tag)", func(t *testing.T) { os.Setenv("MM_SERVICEENVIRONMENT", "") defer os.Unsetenv("MM_SERVICEENVIRONMENT") - require.Equal(t, model.ServiceEnvironmentTest, model.GetServiceEnvironment()) + require.Equal(t, model.ServiceEnvironmentDev, model.GetServiceEnvironment()) }) t.Run("production", func(t *testing.T) { os.Setenv("MM_SERVICEENVIRONMENT", "production") diff --git a/server/public/model/session.go b/server/public/model/session.go index d219909e536..51a747e2e07 100644 --- a/server/public/model/session.go +++ b/server/public/model/session.go @@ -214,6 +214,34 @@ func (s *Session) IsOAuthUser() bool { return isOAuthUser } +func (s *Session) IsBotUser() bool { + val, ok := s.Props[SessionPropIsBot] + if !ok { + return false + } + if val == SessionPropIsBotValue { + return true + } + return false +} + +func (s *Session) IsUserAccessToken() bool { + val, ok := s.Props[SessionPropType] + if !ok { + return false + } + if val == SessionTypeUserAccessToken { + return true + } + return false +} + +// Returns true when session is authenticated as a bot, by personal access token, or is an OAuth app. +// Does not indicate other forms of integrations e.g. webhooks, slash commands, etc. +func (s *Session) IsIntegration() bool { + return s.IsBotUser() || s.IsUserAccessToken() || s.IsOAuth +} + func (s *Session) IsSSOLogin() bool { return s.IsOAuthUser() || s.IsSaml() } diff --git a/server/public/model/session_test.go b/server/public/model/session_test.go index e1814e80e6a..2d2355b7fd2 100644 --- a/server/public/model/session_test.go +++ b/server/public/model/session_test.go @@ -133,3 +133,23 @@ func TestSessionIsOAuthUser(t *testing.T) { }) } } + +func TestIsIntegration(t *testing.T) { + testCases := []struct { + Description string + Session Session + IsIntegration bool + }{ + {"False on empty props", Session{}, false}, + {"True when is OAuth App", Session{IsOAuth: true}, true}, + {"True when session is bot", Session{Props: StringMap{SessionPropIsBot: SessionPropIsBotValue}}, true}, + {"True when session is user access token", Session{Props: StringMap{SessionPropType: SessionTypeUserAccessToken}}, true}, + {"Not affected by Props[UserAuthServiceIsOAuth]", Session{Props: StringMap{UserAuthServiceIsOAuth: strconv.FormatBool(true)}}, false}, + } + + for _, tc := range testCases { + t.Run(tc.Description, func(t *testing.T) { + require.Equal(t, tc.IsIntegration, tc.Session.IsIntegration()) + }) + } +} diff --git a/server/public/model/system.go b/server/public/model/system.go index 98f68d04e16..fb164a88811 100644 --- a/server/public/model/system.go +++ b/server/public/model/system.go @@ -78,36 +78,66 @@ type ServerBusyState struct { } type SupportPacket struct { - LicenseTo string `yaml:"license_to"` - ServerOS string `yaml:"server_os"` - ServerArchitecture string `yaml:"server_architecture"` - ServerVersion string `yaml:"server_version"` - BuildHash string `yaml:"build_hash,omitempty"` - DatabaseType string `yaml:"database_type"` - DatabaseVersion string `yaml:"database_version"` - DatabaseSchemaVersion string `yaml:"database_schema_version"` - LdapVendorName string `yaml:"ldap_vendor_name,omitempty"` - LdapVendorVersion string `yaml:"ldap_vendor_version,omitempty"` - ElasticServerVersion string `yaml:"elastic_server_version,omitempty"` - ElasticServerPlugins []string `yaml:"elastic_server_plugins,omitempty"` - ActiveUsers int `yaml:"active_users"` - LicenseSupportedUsers int `yaml:"license_supported_users,omitempty"` - TotalChannels int `yaml:"total_channels"` - TotalPosts int `yaml:"total_posts"` - TotalTeams int `yaml:"total_teams"` - DailyActiveUsers int `yaml:"daily_active_users"` - MonthlyActiveUsers int `yaml:"monthly_active_users"` - WebsocketConnections int `yaml:"websocket_connections"` - MasterDbConnections int `yaml:"master_db_connections"` - ReplicaDbConnections int `yaml:"read_db_connections"` - InactiveUserCount int `yaml:"inactive_user_count"` - ElasticPostIndexingJobs []*Job `yaml:"elastic_post_indexing_jobs"` - ElasticPostAggregationJobs []*Job `yaml:"elastic_post_aggregation_jobs"` - LdapSyncJobs []*Job `yaml:"ldap_sync_jobs"` - MessageExportJobs []*Job `yaml:"message_export_jobs"` - DataRetentionJobs []*Job `yaml:"data_retention_jobs"` - ComplianceJobs []*Job `yaml:"compliance_jobs"` - MigrationJobs []*Job `yaml:"migration_jobs"` + /* Build information */ + + ServerOS string `yaml:"server_os"` + ServerArchitecture string `yaml:"server_architecture"` + ServerVersion string `yaml:"server_version"` + BuildHash string `yaml:"build_hash,omitempty"` + + /* DB */ + + DatabaseType string `yaml:"database_type"` + DatabaseVersion string `yaml:"database_version"` + DatabaseSchemaVersion string `yaml:"database_schema_version"` + WebsocketConnections int `yaml:"websocket_connections"` + MasterDbConnections int `yaml:"master_db_connections"` + ReplicaDbConnections int `yaml:"read_db_connections"` + + /* Cluster */ + + ClusterID string `yaml:"cluster_id"` + + /* File store */ + + FileDriver string `yaml:"file_driver"` + FileStatus string `yaml:"file_status"` + + /* LDAP */ + + LdapVendorName string `yaml:"ldap_vendor_name,omitempty"` + LdapVendorVersion string `yaml:"ldap_vendor_version,omitempty"` + + /* Elastic Search */ + + ElasticServerVersion string `yaml:"elastic_server_version,omitempty"` + ElasticServerPlugins []string `yaml:"elastic_server_plugins,omitempty"` + + /* License */ + + LicenseTo string `yaml:"license_to"` + LicenseSupportedUsers int `yaml:"license_supported_users,omitempty"` + + /* Server stats */ + + ActiveUsers int `yaml:"active_users"` + DailyActiveUsers int `yaml:"daily_active_users"` + MonthlyActiveUsers int `yaml:"monthly_active_users"` + InactiveUserCount int `yaml:"inactive_user_count"` + TotalPosts int `yaml:"total_posts"` + TotalChannels int `yaml:"total_channels"` + TotalTeams int `yaml:"total_teams"` + + /* Jobs */ + + DataRetentionJobs []*Job `yaml:"data_retention_jobs"` + MessageExportJobs []*Job `yaml:"message_export_jobs"` + ElasticPostIndexingJobs []*Job `yaml:"elastic_post_indexing_jobs"` + ElasticPostAggregationJobs []*Job `yaml:"elastic_post_aggregation_jobs"` + BlevePostIndexingJobs []*Job `yaml:"bleve_post_indexin_jobs"` + LdapSyncJobs []*Job `yaml:"ldap_sync_jobs"` + MigrationJobs []*Job `yaml:"migration_jobs"` + ComplianceJobs []*Job `yaml:"compliance_jobs"` } type FileData struct { diff --git a/server/public/model/user.go b/server/public/model/user.go index 281de1dc30b..b474d515cc6 100644 --- a/server/public/model/user.go +++ b/server/public/model/user.go @@ -62,6 +62,8 @@ const ( UserLocaleMaxLength = 5 UserTimezoneMaxRunes = 256 UserRolesMaxLength = 256 + + DesktopTokenTTL = time.Minute * 3 ) //msgp:tuple User diff --git a/server/public/model/user_count.go b/server/public/model/user_count.go index ee474883a5f..950a6700444 100644 --- a/server/public/model/user_count.go +++ b/server/public/model/user_count.go @@ -9,6 +9,8 @@ type UserCountOptions struct { IncludeBotAccounts bool // Should include deleted users (of any type) IncludeDeleted bool + // Include remote users + IncludeRemoteUsers bool // Exclude regular users ExcludeRegularUsers bool // Only include users on a specific team. "" for any team. diff --git a/server/public/model/version.go b/server/public/model/version.go index 3f24b8531fa..def8fce1dfb 100644 --- a/server/public/model/version.go +++ b/server/public/model/version.go @@ -13,6 +13,8 @@ import ( // It should be maintained in chronological order with most current // release at the front of the list. var versions = []string{ + "9.1.0", + "9.0.0", "8.1.0", "8.0.0", "7.11.0", diff --git a/server/public/model/websocket_message.go b/server/public/model/websocket_message.go index 41cfc51896c..6a8bf5ae31f 100644 --- a/server/public/model/websocket_message.go +++ b/server/public/model/websocket_message.go @@ -48,6 +48,7 @@ const ( WebsocketEventResponse = "response" WebsocketEventEmojiAdded = "emoji_added" WebsocketEventChannelViewed = "channel_viewed" + WebsocketEventMultipleChannelsViewed = "multiple_channels_viewed" WebsocketEventPluginStatusesChanged = "plugin_statuses_changed" WebsocketEventPluginEnabled = "plugin_enabled" WebsocketEventPluginDisabled = "plugin_disabled" diff --git a/server/public/plugin/api.go b/server/public/plugin/api.go index 6c2e2c1649d..f7c96cb2bb6 100644 --- a/server/public/plugin/api.go +++ b/server/public/plugin/api.go @@ -783,6 +783,12 @@ type API interface { // Minimum server version: 5.3 GetFileInfo(fileId string) (*model.FileInfo, *model.AppError) + // SetFileSearchableContent update the File Info searchable text for full text search + // + // @tag File + // Minimum server version: 9.1 + SetFileSearchableContent(fileID string, content string) *model.AppError + // GetFileInfos gets File Infos with options // // @tag File @@ -1180,6 +1186,18 @@ type API interface { // @tag Upload // Minimum server version: 7.6 GetUploadSession(uploadID string) (*model.UploadSession, error) + + // SendPushNotification will send a push notification to all of user's sessions. + // + // It is the responsibility of the plugin to respect the server's configuration and licence, + // especially related to `cfg.EmailSettings.PushNotificationContents`, particularly + // `model.IdLoadedNotification` and the generic settings. + // Refer to `app.sendPushNotificationSync` for the logic used to construct push notifications. + // + // Note: the NotificationWillBePushed hook will be run after SendPushNotification is called. + // + // Minimum server version: 9.0 + SendPushNotification(notification *model.PushNotification, userID string) *model.AppError } var handshake = plugin.HandshakeConfig{ diff --git a/server/public/plugin/api_timer_layer_generated.go b/server/public/plugin/api_timer_layer_generated.go index e3ed4723f37..4376fbbab4e 100644 --- a/server/public/plugin/api_timer_layer_generated.go +++ b/server/public/plugin/api_timer_layer_generated.go @@ -852,6 +852,13 @@ func (api *apiTimerLayer) GetFileInfo(fileId string) (*model.FileInfo, *model.Ap return _returnsA, _returnsB } +func (api *apiTimerLayer) SetFileSearchableContent(fileID string, content string) *model.AppError { + startTime := timePkg.Now() + _returnsA := api.apiImpl.SetFileSearchableContent(fileID, content) + api.recordTime(startTime, "SetFileSearchableContent", _returnsA == nil) + return _returnsA +} + func (api *apiTimerLayer) GetFileInfos(page, perPage int, opt *model.GetFileInfosOptions) ([]*model.FileInfo, *model.AppError) { startTime := timePkg.Now() _returnsA, _returnsB := api.apiImpl.GetFileInfos(page, perPage, opt) @@ -1266,3 +1273,10 @@ func (api *apiTimerLayer) GetUploadSession(uploadID string) (*model.UploadSessio api.recordTime(startTime, "GetUploadSession", _returnsB == nil) return _returnsA, _returnsB } + +func (api *apiTimerLayer) SendPushNotification(notification *model.PushNotification, userID string) *model.AppError { + startTime := timePkg.Now() + _returnsA := api.apiImpl.SendPushNotification(notification, userID) + api.recordTime(startTime, "SendPushNotification", _returnsA == nil) + return _returnsA +} diff --git a/server/public/plugin/client_rpc_generated.go b/server/public/plugin/client_rpc_generated.go index 451692adeca..c91e2df258e 100644 --- a/server/public/plugin/client_rpc_generated.go +++ b/server/public/plugin/client_rpc_generated.go @@ -290,6 +290,40 @@ func (s *hooksRPCServer) MessageHasBeenUpdated(args *Z_MessageHasBeenUpdatedArgs return nil } +func init() { + hookNameToId["MessageHasBeenDeleted"] = MessageHasBeenDeletedID +} + +type Z_MessageHasBeenDeletedArgs struct { + A *Context + B *model.Post +} + +type Z_MessageHasBeenDeletedReturns struct { +} + +func (g *hooksRPCClient) MessageHasBeenDeleted(c *Context, post *model.Post) { + _args := &Z_MessageHasBeenDeletedArgs{c, post} + _returns := &Z_MessageHasBeenDeletedReturns{} + if g.implemented[MessageHasBeenDeletedID] { + if err := g.client.Call("Plugin.MessageHasBeenDeleted", _args, _returns); err != nil { + g.log.Error("RPC call MessageHasBeenDeleted to plugin failed.", mlog.Err(err)) + } + } + +} + +func (s *hooksRPCServer) MessageHasBeenDeleted(args *Z_MessageHasBeenDeletedArgs, returns *Z_MessageHasBeenDeletedReturns) error { + if hook, ok := s.impl.(interface { + MessageHasBeenDeleted(c *Context, post *model.Post) + }); ok { + hook.MessageHasBeenDeleted(args.A, args.B) + } else { + return encodableError(fmt.Errorf("Hook MessageHasBeenDeleted called but not implemented.")) + } + return nil +} + func init() { hookNameToId["ChannelHasBeenCreated"] = ChannelHasBeenCreatedID } @@ -843,6 +877,76 @@ func (s *hooksRPCServer) ConfigurationWillBeSaved(args *Z_ConfigurationWillBeSav return nil } +func init() { + hookNameToId["NotificationWillBePushed"] = NotificationWillBePushedID +} + +type Z_NotificationWillBePushedArgs struct { + A *model.PushNotification + B string +} + +type Z_NotificationWillBePushedReturns struct { + A *model.PushNotification + B string +} + +func (g *hooksRPCClient) NotificationWillBePushed(pushNotification *model.PushNotification, userID string) (*model.PushNotification, string) { + _args := &Z_NotificationWillBePushedArgs{pushNotification, userID} + _returns := &Z_NotificationWillBePushedReturns{} + if g.implemented[NotificationWillBePushedID] { + if err := g.client.Call("Plugin.NotificationWillBePushed", _args, _returns); err != nil { + g.log.Error("RPC call NotificationWillBePushed to plugin failed.", mlog.Err(err)) + } + } + return _returns.A, _returns.B +} + +func (s *hooksRPCServer) NotificationWillBePushed(args *Z_NotificationWillBePushedArgs, returns *Z_NotificationWillBePushedReturns) error { + if hook, ok := s.impl.(interface { + NotificationWillBePushed(pushNotification *model.PushNotification, userID string) (*model.PushNotification, string) + }); ok { + returns.A, returns.B = hook.NotificationWillBePushed(args.A, args.B) + } else { + return encodableError(fmt.Errorf("Hook NotificationWillBePushed called but not implemented.")) + } + return nil +} + +func init() { + hookNameToId["UserHasBeenDeactivated"] = UserHasBeenDeactivatedID +} + +type Z_UserHasBeenDeactivatedArgs struct { + A *Context + B *model.User +} + +type Z_UserHasBeenDeactivatedReturns struct { +} + +func (g *hooksRPCClient) UserHasBeenDeactivated(c *Context, user *model.User) { + _args := &Z_UserHasBeenDeactivatedArgs{c, user} + _returns := &Z_UserHasBeenDeactivatedReturns{} + if g.implemented[UserHasBeenDeactivatedID] { + if err := g.client.Call("Plugin.UserHasBeenDeactivated", _args, _returns); err != nil { + g.log.Error("RPC call UserHasBeenDeactivated to plugin failed.", mlog.Err(err)) + } + } + +} + +func (s *hooksRPCServer) UserHasBeenDeactivated(args *Z_UserHasBeenDeactivatedArgs, returns *Z_UserHasBeenDeactivatedReturns) error { + if hook, ok := s.impl.(interface { + UserHasBeenDeactivated(c *Context, user *model.User) + }); ok { + hook.UserHasBeenDeactivated(args.A, args.B) + } else { + return encodableError(fmt.Errorf("Hook UserHasBeenDeactivated called but not implemented.")) + } + return nil +} + type Z_RegisterCommandArgs struct { A *model.Command } @@ -4275,6 +4379,35 @@ func (s *apiRPCServer) GetFileInfo(args *Z_GetFileInfoArgs, returns *Z_GetFileIn return nil } +type Z_SetFileSearchableContentArgs struct { + A string + B string +} + +type Z_SetFileSearchableContentReturns struct { + A *model.AppError +} + +func (g *apiRPCClient) SetFileSearchableContent(fileID string, content string) *model.AppError { + _args := &Z_SetFileSearchableContentArgs{fileID, content} + _returns := &Z_SetFileSearchableContentReturns{} + if err := g.client.Call("Plugin.SetFileSearchableContent", _args, _returns); err != nil { + log.Printf("RPC call to SetFileSearchableContent API failed: %s", err.Error()) + } + return _returns.A +} + +func (s *apiRPCServer) SetFileSearchableContent(args *Z_SetFileSearchableContentArgs, returns *Z_SetFileSearchableContentReturns) error { + if hook, ok := s.impl.(interface { + SetFileSearchableContent(fileID string, content string) *model.AppError + }); ok { + returns.A = hook.SetFileSearchableContent(args.A, args.B) + } else { + return encodableError(fmt.Errorf("API SetFileSearchableContent called but not implemented.")) + } + return nil +} + type Z_GetFileInfosArgs struct { A int B int @@ -5834,3 +5967,32 @@ func (s *apiRPCServer) GetUploadSession(args *Z_GetUploadSessionArgs, returns *Z } return nil } + +type Z_SendPushNotificationArgs struct { + A *model.PushNotification + B string +} + +type Z_SendPushNotificationReturns struct { + A *model.AppError +} + +func (g *apiRPCClient) SendPushNotification(notification *model.PushNotification, userID string) *model.AppError { + _args := &Z_SendPushNotificationArgs{notification, userID} + _returns := &Z_SendPushNotificationReturns{} + if err := g.client.Call("Plugin.SendPushNotification", _args, _returns); err != nil { + log.Printf("RPC call to SendPushNotification API failed: %s", err.Error()) + } + return _returns.A +} + +func (s *apiRPCServer) SendPushNotification(args *Z_SendPushNotificationArgs, returns *Z_SendPushNotificationReturns) error { + if hook, ok := s.impl.(interface { + SendPushNotification(notification *model.PushNotification, userID string) *model.AppError + }); ok { + returns.A = hook.SendPushNotification(args.A, args.B) + } else { + return encodableError(fmt.Errorf("API SendPushNotification called but not implemented.")) + } + return nil +} diff --git a/server/public/plugin/environment.go b/server/public/plugin/environment.go index 32fd819a551..acf808dea39 100644 --- a/server/public/plugin/environment.go +++ b/server/public/plugin/environment.go @@ -48,16 +48,17 @@ type PrepackagedPlugin struct { // It is meant for use by the Mattermost server to manipulate, interact with and report on the set // of active plugins. type Environment struct { - registeredPlugins sync.Map - pluginHealthCheckJob *PluginHealthCheckJob - logger *mlog.Logger - metrics metricsInterface - newAPIImpl apiImplCreatorFunc - dbDriver Driver - pluginDir string - webappPluginDir string - prepackagedPlugins []*PrepackagedPlugin - prepackagedPluginsLock sync.RWMutex + registeredPlugins sync.Map + pluginHealthCheckJob *PluginHealthCheckJob + logger *mlog.Logger + metrics metricsInterface + newAPIImpl apiImplCreatorFunc + dbDriver Driver + pluginDir string + webappPluginDir string + prepackagedPlugins []*PrepackagedPlugin + transitionallyPrepackagedPlugins []*PrepackagedPlugin + prepackagedPluginsLock sync.RWMutex } func NewEnvironment( @@ -108,7 +109,9 @@ func (env *Environment) Available() ([]*model.BundleInfo, error) { return scanSearchPath(env.pluginDir) } -// Returns a list of prepackaged plugins available in the local prepackaged_plugins folder. +// Returns a list of prepackaged plugins available in the local prepackaged_plugins folder, +// excluding those in transition out of being prepackaged. +// // The list content is immutable and should not be modified. func (env *Environment) PrepackagedPlugins() []*PrepackagedPlugin { env.prepackagedPluginsLock.RLock() @@ -117,6 +120,26 @@ func (env *Environment) PrepackagedPlugins() []*PrepackagedPlugin { return env.prepackagedPlugins } +// TransitionallyPrepackagedPlugins returns a list of plugins transitionally prepackaged in the +// local prepackaged_plugins folder. +// +// The list content is immutable and should not be modified. +func (env *Environment) TransitionallyPrepackagedPlugins() []*PrepackagedPlugin { + env.prepackagedPluginsLock.RLock() + defer env.prepackagedPluginsLock.RUnlock() + + return env.transitionallyPrepackagedPlugins +} + +// ClearTransitionallyPrepackagedPlugins clears the list of plugins transitionally prepackaged +// in the local prepackaged_plugins folder. +func (env *Environment) ClearTransitionallyPrepackagedPlugins() { + env.prepackagedPluginsLock.RLock() + defer env.prepackagedPluginsLock.RUnlock() + + env.transitionallyPrepackagedPlugins = nil +} + // Returns a list of all currently active plugins within the environment. // The returned list should not be modified. func (env *Environment) Active() []*model.BundleInfo { @@ -532,9 +555,10 @@ func (env *Environment) PerformHealthCheck(id string) error { } // SetPrepackagedPlugins saves prepackaged plugins in the environment. -func (env *Environment) SetPrepackagedPlugins(plugins []*PrepackagedPlugin) { +func (env *Environment) SetPrepackagedPlugins(plugins, transitionalPlugins []*PrepackagedPlugin) { env.prepackagedPluginsLock.Lock() env.prepackagedPlugins = plugins + env.transitionallyPrepackagedPlugins = transitionalPlugins env.prepackagedPluginsLock.Unlock() } diff --git a/server/public/plugin/environment_test.go b/server/public/plugin/environment_test.go index 898c2edcca3..adadbf35b42 100644 --- a/server/public/plugin/environment_test.go +++ b/server/public/plugin/environment_test.go @@ -73,26 +73,4 @@ func TestAvailablePlugins(t *testing.T) { require.NoError(t, err) require.Len(t, bundles, 0) }) - - t.Run("Should not load bundles on blocklist", func(t *testing.T) { - bundle := model.BundleInfo{ - Manifest: &model.Manifest{ - Id: "playbooks", - Version: "1", - }, - } - err := os.Mkdir(filepath.Join(dir, "plugin4"), 0700) - require.NoError(t, err) - defer os.RemoveAll(filepath.Join(dir, "plugin4")) - - path := filepath.Join(dir, "plugin4", "plugin.json") - manifestJSON, jsonErr := json.Marshal(bundle.Manifest) - require.NoError(t, jsonErr) - err = os.WriteFile(path, manifestJSON, 0644) - require.NoError(t, err) - - bundles, err := env.Available() - require.NoError(t, err) - require.Len(t, bundles, 0) - }) } diff --git a/server/public/plugin/health_check_test.go b/server/public/plugin/health_check_test.go index 974928ea25a..b05e8be5ca0 100644 --- a/server/public/plugin/health_check_test.go +++ b/server/public/plugin/health_check_test.go @@ -51,10 +51,8 @@ func testPluginHealthCheckSuccess(t *testing.T) { require.NoError(t, err) bundle := model.BundleInfoForPath(dir) - log := mlog.CreateConsoleTestLogger(true, mlog.LvlError) - defer log.Shutdown() - - supervisor, err := newSupervisor(bundle, nil, nil, log, nil) + logger := mlog.CreateConsoleTestLogger(t) + supervisor, err := newSupervisor(bundle, nil, nil, logger, nil) require.NoError(t, err) require.NotNil(t, supervisor) defer supervisor.Shutdown() @@ -94,10 +92,8 @@ func testPluginHealthCheckPanic(t *testing.T) { require.NoError(t, err) bundle := model.BundleInfoForPath(dir) - log := mlog.CreateConsoleTestLogger(true, mlog.LvlError) - defer log.Shutdown() - - supervisor, err := newSupervisor(bundle, nil, nil, log, nil) + logger := mlog.CreateConsoleTestLogger(t) + supervisor, err := newSupervisor(bundle, nil, nil, logger, nil) require.NoError(t, err) require.NotNil(t, supervisor) defer supervisor.Shutdown() diff --git a/server/public/plugin/hooks.go b/server/public/plugin/hooks.go index 5abd46ff32f..8f645ee3379 100644 --- a/server/public/plugin/hooks.go +++ b/server/public/plugin/hooks.go @@ -50,7 +50,10 @@ const ( deprecatedGetCollectionMetadataByIdsID = 32 deprecatedGetTopicMetadataByIdsID = 33 ConfigurationWillBeSavedID = 34 - ServeMetricsID = 35 + NotificationWillBePushedID = 35 + UserHasBeenDeactivatedID = 36 + MessageHasBeenDeletedID = 37 + ServeMetricsID = 38 TotalHooksID = iota ) @@ -168,6 +171,13 @@ type Hooks interface { // Minimum server version: 5.2 MessageHasBeenUpdated(c *Context, newPost, oldPost *model.Post) + // MessageHasBeenDeleted is invoked after the message has been deleted from the database. + // Note that this method will be called for posts deleted by plugins, including the plugin that + // deleted the post. + // + // Minimum server version: 9.1 + MessageHasBeenDeleted(c *Context, post *model.Post) + // ChannelHasBeenCreated is invoked after the channel has been committed to the database. // // Minimum server version: 5.2 @@ -286,10 +296,28 @@ type Hooks interface { // Minimum server version: 8.0 ConfigurationWillBeSaved(newCfg *model.Config) (*model.Config, error) + // NotificationWillBePushed is invoked before a push notification is sent to the push + // notification server. + // + // To reject a notification, return an non-empty string describing why the notification was rejected. + // To modify the notification, return the replacement, non-nil *model.PushNotification and an empty string. + // To allow the notification without modification, return a nil *model.PushNotification and an empty string. + // + // Note that this method will be called for push notifications created by plugins, including the plugin that + // created the notification. + // + // Minimum server version: 9.0 + NotificationWillBePushed(pushNotification *model.PushNotification, userID string) (*model.PushNotification, string) + + // UserHasBeenDeactivated is invoked when a user is deactivated. + // + // Minimum server version: 9.1 + UserHasBeenDeactivated(c *Context, user *model.User) + // ServeMetrics allows plugins to expose their own metrics endpoint through // the server's metrics HTTP listener (e.g. "localhost:8067"). // Requests destined to the /plugins/{id}/metrics path will be routed to the plugin. // - // Minimum server version: 9.0 + // Minimum server version: 9.2 ServeMetrics(c *Context, w http.ResponseWriter, r *http.Request) } diff --git a/server/public/plugin/hooks_timer_layer_generated.go b/server/public/plugin/hooks_timer_layer_generated.go index 45058c1f4d0..7ff0f7f1609 100644 --- a/server/public/plugin/hooks_timer_layer_generated.go +++ b/server/public/plugin/hooks_timer_layer_generated.go @@ -113,6 +113,12 @@ func (hooks *hooksTimerLayer) MessageHasBeenUpdated(c *Context, newPost, oldPost hooks.recordTime(startTime, "MessageHasBeenUpdated", true) } +func (hooks *hooksTimerLayer) MessageHasBeenDeleted(c *Context, post *model.Post) { + startTime := timePkg.Now() + hooks.hooksImpl.MessageHasBeenDeleted(c, post) + hooks.recordTime(startTime, "MessageHasBeenDeleted", true) +} + func (hooks *hooksTimerLayer) ChannelHasBeenCreated(c *Context, channel *model.Channel) { startTime := timePkg.Now() hooks.hooksImpl.ChannelHasBeenCreated(c, channel) @@ -219,6 +225,19 @@ func (hooks *hooksTimerLayer) ConfigurationWillBeSaved(newCfg *model.Config) (*m return _returnsA, _returnsB } +func (hooks *hooksTimerLayer) NotificationWillBePushed(pushNotification *model.PushNotification, userID string) (*model.PushNotification, string) { + startTime := timePkg.Now() + _returnsA, _returnsB := hooks.hooksImpl.NotificationWillBePushed(pushNotification, userID) + hooks.recordTime(startTime, "NotificationWillBePushed", true) + return _returnsA, _returnsB +} + +func (hooks *hooksTimerLayer) UserHasBeenDeactivated(c *Context, user *model.User) { + startTime := timePkg.Now() + hooks.hooksImpl.UserHasBeenDeactivated(c, user) + hooks.recordTime(startTime, "UserHasBeenDeactivated", true) +} + func (hooks *hooksTimerLayer) ServeMetrics(c *Context, w http.ResponseWriter, r *http.Request) { startTime := timePkg.Now() hooks.hooksImpl.ServeMetrics(c, w, r) diff --git a/server/public/plugin/plugintest/api.go b/server/public/plugin/plugintest/api.go index 333b3b5a6b7..c820ee969b5 100644 --- a/server/public/plugin/plugintest/api.go +++ b/server/public/plugin/plugintest/api.go @@ -3639,6 +3639,38 @@ func (_m *API) SendMail(to string, subject string, htmlBody string) *model.AppEr return r0 } +// SendPushNotification provides a mock function with given fields: notification, userID +func (_m *API) SendPushNotification(notification *model.PushNotification, userID string) *model.AppError { + ret := _m.Called(notification, userID) + + var r0 *model.AppError + if rf, ok := ret.Get(0).(func(*model.PushNotification, string) *model.AppError); ok { + r0 = rf(notification, userID) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*model.AppError) + } + } + + return r0 +} + +// SetFileSearchableContent provides a mock function with given fields: fileID, content +func (_m *API) SetFileSearchableContent(fileID string, content string) *model.AppError { + ret := _m.Called(fileID, content) + + var r0 *model.AppError + if rf, ok := ret.Get(0).(func(string, string) *model.AppError); ok { + r0 = rf(fileID, content) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*model.AppError) + } + } + + return r0 +} + // SetProfileImage provides a mock function with given fields: userID, data func (_m *API) SetProfileImage(userID string, data []byte) *model.AppError { ret := _m.Called(userID, data) diff --git a/server/public/plugin/plugintest/hooks.go b/server/public/plugin/plugintest/hooks.go index c3f533fc351..2da3b496a95 100644 --- a/server/public/plugin/plugintest/hooks.go +++ b/server/public/plugin/plugintest/hooks.go @@ -131,6 +131,11 @@ func (_m *Hooks) Implemented() ([]string, error) { return r0, r1 } +// MessageHasBeenDeleted provides a mock function with given fields: c, post +func (_m *Hooks) MessageHasBeenDeleted(c *plugin.Context, post *model.Post) { + _m.Called(c, post) +} + // MessageHasBeenPosted provides a mock function with given fields: c, post func (_m *Hooks) MessageHasBeenPosted(c *plugin.Context, post *model.Post) { _m.Called(c, post) @@ -193,6 +198,32 @@ func (_m *Hooks) MessageWillBeUpdated(c *plugin.Context, newPost *model.Post, ol return r0, r1 } +// NotificationWillBePushed provides a mock function with given fields: pushNotification, userID +func (_m *Hooks) NotificationWillBePushed(pushNotification *model.PushNotification, userID string) (*model.PushNotification, string) { + ret := _m.Called(pushNotification, userID) + + var r0 *model.PushNotification + var r1 string + if rf, ok := ret.Get(0).(func(*model.PushNotification, string) (*model.PushNotification, string)); ok { + return rf(pushNotification, userID) + } + if rf, ok := ret.Get(0).(func(*model.PushNotification, string) *model.PushNotification); ok { + r0 = rf(pushNotification, userID) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*model.PushNotification) + } + } + + if rf, ok := ret.Get(1).(func(*model.PushNotification, string) string); ok { + r1 = rf(pushNotification, userID) + } else { + r1 = ret.Get(1).(string) + } + + return r0, r1 +} + // OnActivate provides a mock function with given fields: func (_m *Hooks) OnActivate() error { ret := _m.Called() @@ -323,6 +354,11 @@ func (_m *Hooks) UserHasBeenCreated(c *plugin.Context, user *model.User) { _m.Called(c, user) } +// UserHasBeenDeactivated provides a mock function with given fields: c, user +func (_m *Hooks) UserHasBeenDeactivated(c *plugin.Context, user *model.User) { + _m.Called(c, user) +} + // UserHasJoinedChannel provides a mock function with given fields: c, channelMember, actor func (_m *Hooks) UserHasJoinedChannel(c *plugin.Context, channelMember *model.ChannelMember, actor *model.User) { _m.Called(c, channelMember, actor) diff --git a/server/public/plugin/product_hooks_generated.go b/server/public/plugin/product_hooks_generated.go index cf591bbc3ec..4a4b3ba83ea 100644 --- a/server/public/plugin/product_hooks_generated.go +++ b/server/public/plugin/product_hooks_generated.go @@ -51,6 +51,10 @@ type MessageHasBeenUpdatedIFace interface { MessageHasBeenUpdated(c *Context, newPost, oldPost *model.Post) } +type MessageHasBeenDeletedIFace interface { + MessageHasBeenDeleted(c *Context, post *model.Post) +} + type ChannelHasBeenCreatedIFace interface { ChannelHasBeenCreated(c *Context, channel *model.Channel) } @@ -119,6 +123,14 @@ type ConfigurationWillBeSavedIFace interface { ConfigurationWillBeSaved(newCfg *model.Config) (*model.Config, error) } +type NotificationWillBePushedIFace interface { + NotificationWillBePushed(pushNotification *model.PushNotification, userID string) (*model.PushNotification, string) +} + +type UserHasBeenDeactivatedIFace interface { + UserHasBeenDeactivated(c *Context, user *model.User) +} + type ServeMetricsIFace interface { ServeMetrics(c *Context, w http.ResponseWriter, r *http.Request) } @@ -217,6 +229,15 @@ func NewAdapter(productHooks any) (*HooksAdapter, error) { return nil, errors.New("hook has MessageHasBeenUpdated method but does not implement plugin.MessageHasBeenUpdated interface") } + // Assessing the type of the productHooks if it individually implements MessageHasBeenDeleted interface. + tt = reflect.TypeOf((*MessageHasBeenDeletedIFace)(nil)).Elem() + + if ft.Implements(tt) { + a.implemented[MessageHasBeenDeletedID] = struct{}{} + } else if _, ok := ft.MethodByName("MessageHasBeenDeleted"); ok { + return nil, errors.New("hook has MessageHasBeenDeleted method but does not implement plugin.MessageHasBeenDeleted interface") + } + // Assessing the type of the productHooks if it individually implements ChannelHasBeenCreated interface. tt = reflect.TypeOf((*ChannelHasBeenCreatedIFace)(nil)).Elem() @@ -370,6 +391,24 @@ func NewAdapter(productHooks any) (*HooksAdapter, error) { return nil, errors.New("hook has ConfigurationWillBeSaved method but does not implement plugin.ConfigurationWillBeSaved interface") } + // Assessing the type of the productHooks if it individually implements NotificationWillBePushed interface. + tt = reflect.TypeOf((*NotificationWillBePushedIFace)(nil)).Elem() + + if ft.Implements(tt) { + a.implemented[NotificationWillBePushedID] = struct{}{} + } else if _, ok := ft.MethodByName("NotificationWillBePushed"); ok { + return nil, errors.New("hook has NotificationWillBePushed method but does not implement plugin.NotificationWillBePushed interface") + } + + // Assessing the type of the productHooks if it individually implements UserHasBeenDeactivated interface. + tt = reflect.TypeOf((*UserHasBeenDeactivatedIFace)(nil)).Elem() + + if ft.Implements(tt) { + a.implemented[UserHasBeenDeactivatedID] = struct{}{} + } else if _, ok := ft.MethodByName("UserHasBeenDeactivated"); ok { + return nil, errors.New("hook has UserHasBeenDeactivated method but does not implement plugin.UserHasBeenDeactivated interface") + } + // Assessing the type of the productHooks if it individually implements ServeMetrics interface. tt = reflect.TypeOf((*ServeMetricsIFace)(nil)).Elem() @@ -463,6 +502,15 @@ func (a *HooksAdapter) MessageHasBeenUpdated(c *Context, newPost, oldPost *model } +func (a *HooksAdapter) MessageHasBeenDeleted(c *Context, post *model.Post) { + if _, ok := a.implemented[MessageHasBeenDeletedID]; !ok { + panic("product hooks must implement MessageHasBeenDeleted") + } + + a.productHooks.(MessageHasBeenDeletedIFace).MessageHasBeenDeleted(c, post) + +} + func (a *HooksAdapter) ChannelHasBeenCreated(c *Context, channel *model.Channel) { if _, ok := a.implemented[ChannelHasBeenCreatedID]; !ok { panic("product hooks must implement ChannelHasBeenCreated") @@ -616,6 +664,24 @@ func (a *HooksAdapter) ConfigurationWillBeSaved(newCfg *model.Config) (*model.Co } +func (a *HooksAdapter) NotificationWillBePushed(pushNotification *model.PushNotification, userID string) (*model.PushNotification, string) { + if _, ok := a.implemented[NotificationWillBePushedID]; !ok { + panic("product hooks must implement NotificationWillBePushed") + } + + return a.productHooks.(NotificationWillBePushedIFace).NotificationWillBePushed(pushNotification, userID) + +} + +func (a *HooksAdapter) UserHasBeenDeactivated(c *Context, user *model.User) { + if _, ok := a.implemented[UserHasBeenDeactivatedID]; !ok { + panic("product hooks must implement UserHasBeenDeactivated") + } + + a.productHooks.(UserHasBeenDeactivatedIFace).UserHasBeenDeactivated(c, user) + +} + func (a *HooksAdapter) ServeMetrics(c *Context, w http.ResponseWriter, r *http.Request) { if _, ok := a.implemented[ServeMetricsID]; !ok { panic("product hooks must implement ServeMetrics") diff --git a/server/public/plugin/supervisor_test.go b/server/public/plugin/supervisor_test.go index 364d4cb01bb..d9dfeb8b279 100644 --- a/server/public/plugin/supervisor_test.go +++ b/server/public/plugin/supervisor_test.go @@ -34,9 +34,8 @@ func testSupervisorInvalidExecutablePath(t *testing.T) { os.WriteFile(filepath.Join(dir, "plugin.json"), []byte(`{"id": "foo", "server": {"executable": "/foo/../../backend.exe"}}`), 0600) bundle := model.BundleInfoForPath(dir) - log := mlog.CreateConsoleTestLogger(true, mlog.LvlError) - defer log.Shutdown() - supervisor, err := newSupervisor(bundle, nil, nil, log, nil) + logger := mlog.CreateConsoleTestLogger(t) + supervisor, err := newSupervisor(bundle, nil, nil, logger, nil) assert.Nil(t, supervisor) assert.Error(t, err) } @@ -49,9 +48,8 @@ func testSupervisorNonExistentExecutablePath(t *testing.T) { os.WriteFile(filepath.Join(dir, "plugin.json"), []byte(`{"id": "foo", "server": {"executable": "thisfileshouldnotexist"}}`), 0600) bundle := model.BundleInfoForPath(dir) - log := mlog.CreateConsoleTestLogger(true, mlog.LvlError) - defer log.Shutdown() - supervisor, err := newSupervisor(bundle, nil, nil, log, nil) + logger := mlog.CreateConsoleTestLogger(t) + supervisor, err := newSupervisor(bundle, nil, nil, logger, nil) require.Error(t, err) require.Nil(t, supervisor) } @@ -75,9 +73,8 @@ func testSupervisorStartTimeout(t *testing.T) { os.WriteFile(filepath.Join(dir, "plugin.json"), []byte(`{"id": "foo", "server": {"executable": "backend.exe"}}`), 0600) bundle := model.BundleInfoForPath(dir) - log := mlog.CreateConsoleTestLogger(true, mlog.LvlError) - defer log.Shutdown() - supervisor, err := newSupervisor(bundle, nil, nil, log, nil) + logger := mlog.CreateConsoleTestLogger(t) + supervisor, err := newSupervisor(bundle, nil, nil, logger, nil) require.Error(t, err) require.Nil(t, supervisor) } diff --git a/server/public/pluginapi/bot.go b/server/public/pluginapi/bot.go new file mode 100644 index 00000000000..74a7710b74e --- /dev/null +++ b/server/public/pluginapi/bot.go @@ -0,0 +1,202 @@ +package pluginapi + +import ( + "os" + "path/filepath" + + "github.com/pkg/errors" + + "github.com/mattermost/mattermost/server/public/model" + "github.com/mattermost/mattermost/server/public/plugin" + "github.com/mattermost/mattermost/server/public/pluginapi/cluster" +) + +const ( + internalKeyPrefix = "mmi_" + botUserKey = internalKeyPrefix + "botid" + botEnsureMutexKey = internalKeyPrefix + "bot_ensure" +) + +// BotService exposes methods to manipulate bots. +type BotService struct { + api plugin.API +} + +// Get returns a bot by botUserID. +// +// Minimum server version: 5.10 +func (b *BotService) Get(botUserID string, includeDeleted bool) (*model.Bot, error) { + bot, appErr := b.api.GetBot(botUserID, includeDeleted) + + return bot, normalizeAppErr(appErr) +} + +// BotListOption is an option to configure a bot List() request. +type BotListOption func(*model.BotGetOptions) + +// BotOwner option configures bot list request to only retrieve the bots that matches with +// owner's id. +func BotOwner(id string) BotListOption { + return func(o *model.BotGetOptions) { + o.OwnerId = id + } +} + +// BotIncludeDeleted option configures bot list request to also retrieve the deleted bots. +func BotIncludeDeleted() BotListOption { + return func(o *model.BotGetOptions) { + o.IncludeDeleted = true + } +} + +// BotOnlyOrphans option configures bot list request to only retrieve orphan bots. +func BotOnlyOrphans() BotListOption { + return func(o *model.BotGetOptions) { + o.OnlyOrphaned = true + } +} + +// List returns a list of bots by page, count and options. +// +// Minimum server version: 5.10 +func (b *BotService) List(page, perPage int, options ...BotListOption) ([]*model.Bot, error) { + opts := &model.BotGetOptions{ + Page: page, + PerPage: perPage, + } + for _, o := range options { + o(opts) + } + bots, appErr := b.api.GetBots(opts) + + return bots, normalizeAppErr(appErr) +} + +// Create creates the bot and corresponding user. +// +// Minimum server version: 5.10 +func (b *BotService) Create(bot *model.Bot) error { + createdBot, appErr := b.api.CreateBot(bot) + if appErr != nil { + return normalizeAppErr(appErr) + } + + *bot = *createdBot + + return nil +} + +// Patch applies the given patch to the bot and corresponding user. +// +// Minimum server version: 5.10 +func (b *BotService) Patch(botUserID string, botPatch *model.BotPatch) (*model.Bot, error) { + bot, appErr := b.api.PatchBot(botUserID, botPatch) + + return bot, normalizeAppErr(appErr) +} + +// UpdateActive marks a bot as active or inactive, along with its corresponding user. +// +// Minimum server version: 5.10 +func (b *BotService) UpdateActive(botUserID string, isActive bool) (*model.Bot, error) { + bot, appErr := b.api.UpdateBotActive(botUserID, isActive) + + return bot, normalizeAppErr(appErr) +} + +// DeletePermanently permanently deletes a bot and its corresponding user. +// +// Minimum server version: 5.10 +func (b *BotService) DeletePermanently(botUserID string) error { + return normalizeAppErr(b.api.PermanentDeleteBot(botUserID)) +} + +type ensureBotOptions struct { + ProfileImagePath string +} + +type EnsureBotOption func(*ensureBotOptions) + +func ProfileImagePath(path string) EnsureBotOption { + return func(args *ensureBotOptions) { + args.ProfileImagePath = path + } +} + +// EnsureBot either returns an existing bot user matching the given bot, or creates a bot user from the given bot. +// A profile image or icon image may be optionally passed in to be set for the existing or newly created bot. +// Returns the id of the resulting bot. +// EnsureBot can safely be called multiple instances of a plugin concurrently. +// +// Minimum server version: 5.10 +func (b *BotService) EnsureBot(bot *model.Bot, options ...EnsureBotOption) (string, error) { + m, err := cluster.NewMutex(b.api, botEnsureMutexKey) + if err != nil { + return "", errors.Wrap(err, "failed to create mutex") + } + + return b.ensureBot(m, bot, options...) +} + +type mutex interface { + Lock() + Unlock() +} + +// TODO: this utility function is also used by the product framework. We should move this to mattermost-server and share +// the code to maintain consistent behavior. Ticket: MM-44953 +func (b *BotService) ensureBot(m mutex, bot *model.Bot, options ...EnsureBotOption) (string, error) { + err := ensureServerVersion(b.api, "5.10.0") + if err != nil { + return "", errors.Wrap(err, "failed to ensure bot") + } + + // Default options + o := &ensureBotOptions{ + ProfileImagePath: "", + } + + for _, setter := range options { + setter(o) + } + + botID, err := b.ensureBotUser(m, bot) + if err != nil { + return "", err + } + + if o.ProfileImagePath != "" { + imageBytes, err := b.readFile(o.ProfileImagePath) + if err != nil { + return "", errors.Wrap(err, "failed to read profile image") + } + appErr := b.api.SetProfileImage(botID, imageBytes) + if appErr != nil { + return "", errors.Wrap(appErr, "failed to set profile image") + } + } + + return botID, nil +} + +func (b *BotService) ensureBotUser(m mutex, bot *model.Bot) (retBotID string, retErr error) { + // Lock to prevent two plugins from racing to create the bot account + m.Lock() + defer m.Unlock() + + return b.api.EnsureBotUser(bot) +} + +func (b *BotService) readFile(path string) ([]byte, error) { + bundlePath, err := b.api.GetBundlePath() + if err != nil { + return nil, errors.Wrap(err, "failed to get bundle path") + } + + imageBytes, err := os.ReadFile(filepath.Join(bundlePath, path)) + if err != nil { + return nil, errors.Wrap(err, "failed to read image") + } + + return imageBytes, nil +} diff --git a/server/public/pluginapi/bot_test.go b/server/public/pluginapi/bot_test.go new file mode 100644 index 00000000000..cbcf321ebc2 --- /dev/null +++ b/server/public/pluginapi/bot_test.go @@ -0,0 +1,368 @@ +package pluginapi + +import ( + "net/http" + "os" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/mattermost/mattermost/server/public/model" + "github.com/mattermost/mattermost/server/public/plugin/plugintest" +) + +func TestCreateBot(t *testing.T) { + t.Run("success", func(t *testing.T) { + api := &plugintest.API{} + defer api.AssertExpectations(t) + client := NewClient(api, &plugintest.Driver{}) + + api.On("CreateBot", &model.Bot{Username: "1"}).Return(&model.Bot{Username: "1", UserId: "2"}, nil) + + bot := &model.Bot{Username: "1"} + err := client.Bot.Create(bot) + require.NoError(t, err) + require.Equal(t, &model.Bot{Username: "1", UserId: "2"}, bot) + }) + + t.Run("failure", func(t *testing.T) { + api := &plugintest.API{} + defer api.AssertExpectations(t) + client := NewClient(api, &plugintest.Driver{}) + + appErr := newAppError() + + api.On("CreateBot", &model.Bot{Username: "1"}).Return(nil, appErr) + + bot := &model.Bot{Username: "1"} + err := client.Bot.Create(&model.Bot{Username: "1"}) + require.Equal(t, appErr, err) + require.Equal(t, &model.Bot{Username: "1"}, bot) + }) +} + +func TestUpdateBotStatus(t *testing.T) { + t.Run("success", func(t *testing.T) { + api := &plugintest.API{} + defer api.AssertExpectations(t) + client := NewClient(api, &plugintest.Driver{}) + + api.On("UpdateBotActive", "1", true).Return(&model.Bot{UserId: "2"}, nil) + + bot, err := client.Bot.UpdateActive("1", true) + require.NoError(t, err) + require.Equal(t, &model.Bot{UserId: "2"}, bot) + }) + + t.Run("failure", func(t *testing.T) { + api := &plugintest.API{} + defer api.AssertExpectations(t) + client := NewClient(api, &plugintest.Driver{}) + + appErr := newAppError() + + api.On("UpdateBotActive", "1", true).Return(nil, appErr) + + bot, err := client.Bot.UpdateActive("1", true) + require.Equal(t, appErr, err) + require.Zero(t, bot) + }) +} + +func TestGetBot(t *testing.T) { + t.Run("success", func(t *testing.T) { + api := &plugintest.API{} + defer api.AssertExpectations(t) + client := NewClient(api, &plugintest.Driver{}) + + api.On("GetBot", "1", true).Return(&model.Bot{UserId: "2"}, nil) + + bot, err := client.Bot.Get("1", true) + require.NoError(t, err) + require.Equal(t, &model.Bot{UserId: "2"}, bot) + }) + + t.Run("failure", func(t *testing.T) { + api := &plugintest.API{} + defer api.AssertExpectations(t) + client := NewClient(api, &plugintest.Driver{}) + + appErr := newAppError() + + api.On("GetBot", "1", true).Return(nil, appErr) + + bot, err := client.Bot.Get("1", true) + require.Equal(t, appErr, err) + require.Zero(t, bot) + }) +} + +func TestListBot(t *testing.T) { + tests := []struct { + name string + page, count int + options []BotListOption + expectedOptions *model.BotGetOptions + bots []*model.Bot + err error + }{ + { + "owner filter", + 1, + 2, + []BotListOption{ + BotOwner("3"), + }, + &model.BotGetOptions{ + Page: 1, + PerPage: 2, + OwnerId: "3", + }, + []*model.Bot{ + {UserId: "4"}, + {UserId: "5"}, + }, + nil, + }, + { + "all filter", + 1, + 2, + []BotListOption{ + BotOwner("3"), + BotIncludeDeleted(), + BotOnlyOrphans(), + }, + &model.BotGetOptions{ + Page: 1, + PerPage: 2, + OwnerId: "3", + IncludeDeleted: true, + OnlyOrphaned: true, + }, + []*model.Bot{ + {UserId: "4"}, + }, + nil, + }, + { + "no filter", + 1, + 2, + []BotListOption{}, + &model.BotGetOptions{ + Page: 1, + PerPage: 2, + }, + []*model.Bot{ + {UserId: "4"}, + }, + nil, + }, + { + "app error", + 1, + 2, + []BotListOption{ + BotOwner("3"), + }, + &model.BotGetOptions{ + Page: 1, + PerPage: 2, + OwnerId: "3", + }, + nil, + newAppError(), + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + api := &plugintest.API{} + client := NewClient(api, &plugintest.Driver{}) + + api.On("GetBots", test.expectedOptions).Return(test.bots, test.err) + + bots, err := client.Bot.List(test.page, test.count, test.options...) + if test.err != nil { + require.Equal(t, test.err.Error(), err.Error(), test.name) + } else { + require.NoError(t, err, test.name) + } + require.Equal(t, test.bots, bots, test.name) + + api.AssertExpectations(t) + }) + } +} + +func TestDeleteBotPermanently(t *testing.T) { + t.Run("success", func(t *testing.T) { + api := &plugintest.API{} + defer api.AssertExpectations(t) + client := NewClient(api, &plugintest.Driver{}) + + api.On("PermanentDeleteBot", "1").Return(nil) + + err := client.Bot.DeletePermanently("1") + require.NoError(t, err) + }) + + t.Run("failure", func(t *testing.T) { + api := &plugintest.API{} + defer api.AssertExpectations(t) + client := NewClient(api, &plugintest.Driver{}) + + appErr := newAppError() + + api.On("PermanentDeleteBot", "1").Return(appErr) + + err := client.Bot.DeletePermanently("1") + require.Equal(t, appErr, err) + }) +} + +func TestEnsureBot(t *testing.T) { + testbot := &model.Bot{ + Username: "testbot", + DisplayName: "Test Bot", + Description: "testbotdescription", + } + + m := testMutex{} + + t.Run("server version incompatible", func(t *testing.T) { + api := &plugintest.API{} + defer api.AssertExpectations(t) + client := NewClient(api, &plugintest.Driver{}) + + api.On("GetServerVersion").Return("5.9.0") + + _, err := client.Bot.ensureBot(m, nil) + require.Error(t, err) + assert.Equal(t, + "failed to ensure bot: incompatible server version for plugin, minimum required version: 5.10.0, current version: 5.9.0", + err.Error(), + ) + }) + + t.Run("if bot already exists", func(t *testing.T) { + t.Run("should find and return the existing bot ID", func(t *testing.T) { + api := &plugintest.API{} + defer api.AssertExpectations(t) + client := NewClient(api, &plugintest.Driver{}) + + expectedBotID := model.NewId() + + api.On("GetServerVersion").Return("5.10.0") + api.On("EnsureBotUser", testbot).Return(expectedBotID, nil) + botID, err := client.Bot.ensureBot(m, testbot) + + require.NoError(t, err) + assert.Equal(t, expectedBotID, botID) + }) + + t.Run("should set the bot profile image when specified", func(t *testing.T) { + api := &plugintest.API{} + defer api.AssertExpectations(t) + client := NewClient(api, &plugintest.Driver{}) + + expectedBotID := model.NewId() + + profileImageFile, err := os.CreateTemp("", "profile_image") + require.NoError(t, err) + + profileImageBytes := []byte("profile image") + err = os.WriteFile(profileImageFile.Name(), profileImageBytes, 0o600) + require.NoError(t, err) + + api.On("GetBundlePath").Return("", nil) + api.On("EnsureBotUser", testbot).Return(expectedBotID, nil) + api.On("SetProfileImage", expectedBotID, profileImageBytes).Return(nil) + api.On("GetServerVersion").Return("5.10.0") + + botID, err := client.Bot.ensureBot(m, testbot, ProfileImagePath(profileImageFile.Name())) + require.NoError(t, err) + assert.Equal(t, expectedBotID, botID) + }) + + t.Run("should find and update the bot with new bot details", func(t *testing.T) { + api := &plugintest.API{} + defer api.AssertExpectations(t) + client := NewClient(api, &plugintest.Driver{}) + + expectedBotID := model.NewId() + expectedBotUsername := "updated_testbot" + expectedBotDisplayName := "Updated Test Bot" + expectedBotDescription := "updated testbotdescription" + + profileImageFile, err := os.CreateTemp("", "profile_image") + require.NoError(t, err) + + profileImageBytes := []byte("profile image") + err = os.WriteFile(profileImageFile.Name(), profileImageBytes, 0o600) + require.NoError(t, err) + + iconImageFile, err := os.CreateTemp("", "profile_image") + require.NoError(t, err) + + iconImageBytes := []byte("icon image") + err = os.WriteFile(iconImageFile.Name(), iconImageBytes, 0o600) + require.NoError(t, err) + + updatedTestBot := &model.Bot{ + Username: expectedBotUsername, + DisplayName: expectedBotDisplayName, + Description: expectedBotDescription, + } + api.On("GetServerVersion").Return("5.10.0") + api.On("EnsureBotUser", updatedTestBot).Return(expectedBotID, nil) + api.On("GetBundlePath").Return("", nil) + api.On("SetProfileImage", expectedBotID, profileImageBytes).Return(nil) + + botID, err := client.Bot.ensureBot(m, + updatedTestBot, + ProfileImagePath(profileImageFile.Name()), + ) + require.NoError(t, err) + assert.Equal(t, expectedBotID, botID) + }) + }) + + t.Run("if bot doesn't exist", func(t *testing.T) { + t.Run("should create bot and set the bot profile image when specified", func(t *testing.T) { + api := &plugintest.API{} + defer api.AssertExpectations(t) + client := NewClient(api, &plugintest.Driver{}) + + expectedBotID := model.NewId() + + profileImageFile, err := os.CreateTemp("", "profile_image") + require.NoError(t, err) + + profileImageBytes := []byte("profile image") + err = os.WriteFile(profileImageFile.Name(), profileImageBytes, 0o600) + require.NoError(t, err) + + api.On("EnsureBotUser", testbot).Return(expectedBotID, nil) + api.On("GetBundlePath").Return("", nil) + api.On("SetProfileImage", expectedBotID, profileImageBytes).Return(nil) + api.On("GetServerVersion").Return("5.10.0") + + botID, err := client.Bot.ensureBot(m, testbot, ProfileImagePath(profileImageFile.Name())) + require.NoError(t, err) + assert.Equal(t, expectedBotID, botID) + }) + }) +} + +func newAppError() *model.AppError { + return model.NewAppError("here", "id", nil, "an error occurred", http.StatusInternalServerError) +} + +type testMutex struct { +} + +func (m testMutex) Lock() {} +func (m testMutex) Unlock() {} diff --git a/server/public/pluginapi/channel.go b/server/public/pluginapi/channel.go new file mode 100644 index 00000000000..ecf9b5ad87d --- /dev/null +++ b/server/public/pluginapi/channel.go @@ -0,0 +1,286 @@ +package pluginapi + +import ( + "net/http" + "time" + + "github.com/pkg/errors" + + "github.com/mattermost/mattermost/server/public/model" + "github.com/mattermost/mattermost/server/public/plugin" +) + +// ChannelService exposes methods to manipulate channels. +type ChannelService struct { + api plugin.API +} + +// Get gets a channel. +// +// Minimum server version: 5.2 +func (c *ChannelService) Get(channelID string) (*model.Channel, error) { + channel, appErr := c.api.GetChannel(channelID) + + return channel, normalizeAppErr(appErr) +} + +// GetByName gets a channel by its name, given a team id. +// +// Minimum server version: 5.2 +func (c *ChannelService) GetByName(teamID, channelName string, includeDeleted bool) (*model.Channel, error) { + channel, appErr := c.api.GetChannelByName(teamID, channelName, includeDeleted) + + return channel, normalizeAppErr(appErr) +} + +// GetDirect gets a direct message channel. +// +// Note that if the channel does not exist it will create it. +// +// Minimum server version: 5.2 +func (c *ChannelService) GetDirect(userID1, userID2 string) (*model.Channel, error) { + channel, appErr := c.api.GetDirectChannel(userID1, userID2) + + return channel, normalizeAppErr(appErr) +} + +// GetGroup gets a group message channel. +// +// Note that if the channel does not exist it will create it. +// +// Minimum server version: 5.2 +func (c *ChannelService) GetGroup(userIDs []string) (*model.Channel, error) { + channel, appErr := c.api.GetGroupChannel(userIDs) + + return channel, normalizeAppErr(appErr) +} + +// GetByNameForTeamName gets a channel by its name, given a team name. +// +// Minimum server version: 5.2 +func (c *ChannelService) GetByNameForTeamName(teamName, channelName string, includeDeleted bool) (*model.Channel, error) { + channel, appErr := c.api.GetChannelByNameForTeamName(teamName, channelName, includeDeleted) + + return channel, normalizeAppErr(appErr) +} + +// ListForTeamForUser gets a list of channels for given user ID in given team ID. +// +// Minimum server version: 5.6 +func (c *ChannelService) ListForTeamForUser(teamID, userID string, includeDeleted bool) ([]*model.Channel, error) { + channels, appErr := c.api.GetChannelsForTeamForUser(teamID, userID, includeDeleted) + + return channels, normalizeAppErr(appErr) +} + +// ListPublicChannelsForTeam gets a list of all channels. +// +// Minimum server version: 5.2 +func (c *ChannelService) ListPublicChannelsForTeam(teamID string, page, perPage int) ([]*model.Channel, error) { + channels, appErr := c.api.GetPublicChannelsForTeam(teamID, page, perPage) + + return channels, normalizeAppErr(appErr) +} + +// Search returns the channels on a team matching the provided search term. +// +// Minimum server version: 5.6 +func (c *ChannelService) Search(teamID, term string) ([]*model.Channel, error) { + channels, appErr := c.api.SearchChannels(teamID, term) + + return channels, normalizeAppErr(appErr) +} + +// Create creates a channel. +// +// Minimum server version: 5.2 +func (c *ChannelService) Create(channel *model.Channel) error { + createdChannel, appErr := c.api.CreateChannel(channel) + if appErr != nil { + return normalizeAppErr(appErr) + } + + *channel = *createdChannel + + return c.waitForChannelCreation(channel.Id) +} + +// Update updates a channel. +// +// Minimum server version: 5.2 +func (c *ChannelService) Update(channel *model.Channel) error { + updatedChannel, appErr := c.api.UpdateChannel(channel) + if appErr != nil { + return normalizeAppErr(appErr) + } + + *channel = *updatedChannel + + return nil +} + +// Delete deletes a channel. +// +// Minimum server version: 5.2 +func (c *ChannelService) Delete(channelID string) error { + return normalizeAppErr(c.api.DeleteChannel(channelID)) +} + +// GetChannelStats gets statistics for a channel. +// +// Minimum server version: 5.6 +func (c *ChannelService) GetChannelStats(channelID string) (*model.ChannelStats, error) { + channelStats, appErr := c.api.GetChannelStats(channelID) + + return channelStats, normalizeAppErr(appErr) +} + +// GetMember gets a channel membership for a user. +// +// Minimum server version: 5.2 +func (c *ChannelService) GetMember(channelID, userID string) (*model.ChannelMember, error) { + channelMember, appErr := c.api.GetChannelMember(channelID, userID) + + return channelMember, normalizeAppErr(appErr) +} + +// ListMembers gets a channel membership for all users. +// +// Minimum server version: 5.6 +func (c *ChannelService) ListMembers(channelID string, page, perPage int) ([]*model.ChannelMember, error) { + channelMembers, appErr := c.api.GetChannelMembers(channelID, page, perPage) + + return channelMembersToChannelMemberSlice(channelMembers), normalizeAppErr(appErr) +} + +// ListMembersByIDs gets a channel membership for a particular User +// +// Minimum server version: 5.6 +func (c *ChannelService) ListMembersByIDs(channelID string, userIDs []string) ([]*model.ChannelMember, error) { + channelMembers, appErr := c.api.GetChannelMembersByIds(channelID, userIDs) + + return channelMembersToChannelMemberSlice(channelMembers), normalizeAppErr(appErr) +} + +// ListMembersForUser returns all channel memberships on a team for a user. +// +// Minimum server version: 5.10 +func (c *ChannelService) ListMembersForUser(teamID, userID string, page, perPage int) ([]*model.ChannelMember, error) { + channelMembers, appErr := c.api.GetChannelMembersForUser(teamID, userID, page, perPage) + + return channelMembers, normalizeAppErr(appErr) +} + +// AddMember joins a user to a channel (as if they joined themselves). +// This means the user will not receive notifications for joining the channel. +// +// Minimum server version: 5.2 +func (c *ChannelService) AddMember(channelID, userID string) (*model.ChannelMember, error) { + channelMember, appErr := c.api.AddChannelMember(channelID, userID) + + return channelMember, normalizeAppErr(appErr) +} + +// AddUser adds a user to a channel as if the specified user had invited them. +// This means the user will receive the regular notifications for being added to the channel. +// +// Minimum server version: 5.18 +func (c *ChannelService) AddUser(channelID, userID, asUserID string) (*model.ChannelMember, error) { + channelMember, appErr := c.api.AddUserToChannel(channelID, userID, asUserID) + + return channelMember, normalizeAppErr(appErr) +} + +// DeleteMember deletes a channel membership for a user. +// +// Minimum server version: 5.2 +func (c *ChannelService) DeleteMember(channelID, userID string) error { + appErr := c.api.DeleteChannelMember(channelID, userID) + + return normalizeAppErr(appErr) +} + +// UpdateChannelMemberRoles updates a user's roles for a channel. +// +// Minimum server version: 5.2 +func (c *ChannelService) UpdateChannelMemberRoles(channelID, userID, newRoles string) (*model.ChannelMember, error) { + channelMember, appErr := c.api.UpdateChannelMemberRoles(channelID, userID, newRoles) + + return channelMember, normalizeAppErr(appErr) +} + +// UpdateChannelMemberNotifications updates a user's notification properties for a channel. +// +// Minimum server version: 5.2 +func (c *ChannelService) UpdateChannelMemberNotifications(channelID, userID string, notifications map[string]string) (*model.ChannelMember, error) { + channelMember, appErr := c.api.UpdateChannelMemberNotifications(channelID, userID, notifications) + + return channelMember, normalizeAppErr(appErr) +} + +// CreateSidebarCategory creates a new sidebar category for a set of channels. +// +// Minimum server version: 5.38 +func (c *ChannelService) CreateSidebarCategory( + userID, teamID string, newCategory *model.SidebarCategoryWithChannels) error { + category, appErr := c.api.CreateChannelSidebarCategory(userID, teamID, newCategory) + if appErr != nil { + return normalizeAppErr(appErr) + } + *newCategory = *category + + return nil +} + +// GetSidebarCategories returns sidebar categories. +// +// Minimum server version: 5.38 +func (c *ChannelService) GetSidebarCategories(userID, teamID string) (*model.OrderedSidebarCategories, error) { + categories, appErr := c.api.GetChannelSidebarCategories(userID, teamID) + + return categories, normalizeAppErr(appErr) +} + +// UpdateSidebarCategories updates the channel sidebar categories. +// +// Minimum server version: 5.38 +func (c *ChannelService) UpdateSidebarCategories( + userID, teamID string, categories []*model.SidebarCategoryWithChannels) error { + updatedCategories, appErr := c.api.UpdateChannelSidebarCategories(userID, teamID, categories) + if appErr != nil { + return normalizeAppErr(appErr) + } + copy(categories, updatedCategories) + + return nil +} + +func (c *ChannelService) waitForChannelCreation(channelID string) error { + if len(c.api.GetConfig().SqlSettings.DataSourceReplicas) == 0 { + return nil + } + + now := time.Now() + + for time.Since(now) < 1500*time.Millisecond { + time.Sleep(100 * time.Millisecond) + + if _, err := c.api.GetChannel(channelID); err == nil { + // Channel found + return nil + } else if err.StatusCode != http.StatusNotFound { + return err + } + } + + return errors.Errorf("giving up waiting for channel creation, channelID=%s", channelID) +} + +func channelMembersToChannelMemberSlice(cm model.ChannelMembers) []*model.ChannelMember { + cmp := make([]*model.ChannelMember, len(cm)) + for i := 0; i < len(cm); i++ { + cmp[i] = &(cm)[i] + } + + return cmp +} diff --git a/server/public/pluginapi/channel_test.go b/server/public/pluginapi/channel_test.go new file mode 100644 index 00000000000..2e6803ca1f0 --- /dev/null +++ b/server/public/pluginapi/channel_test.go @@ -0,0 +1,373 @@ +package pluginapi_test + +import ( + "net/http" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/mattermost/mattermost/server/public/model" + "github.com/mattermost/mattermost/server/public/plugin/plugintest" + "github.com/mattermost/mattermost/server/public/pluginapi" +) + +func TestGetMembers(t *testing.T) { + t.Run("empty list", func(t *testing.T) { + api := &plugintest.API{} + defer api.AssertExpectations(t) + client := pluginapi.NewClient(api, &plugintest.Driver{}) + + api.On("GetChannelMembers", "channelID", 1, 10).Return(nil, nil) + + cm, err := client.Channel.ListMembers("channelID", 1, 10) + require.NoError(t, err) + require.Empty(t, cm) + }) +} + +func TestGetTeamChannelByName(t *testing.T) { + t.Run("success", func(t *testing.T) { + api := &plugintest.API{} + defer api.AssertExpectations(t) + client := pluginapi.NewClient(api, &plugintest.Driver{}) + + api.On("GetChannelByNameForTeamName", "1", "2", true).Return(&model.Channel{TeamId: "3"}, nil) + + channel, err := client.Channel.GetByNameForTeamName("1", "2", true) + require.NoError(t, err) + require.Equal(t, &model.Channel{TeamId: "3"}, channel) + }) + + t.Run("failure", func(t *testing.T) { + api := &plugintest.API{} + defer api.AssertExpectations(t) + client := pluginapi.NewClient(api, &plugintest.Driver{}) + + api.On("GetChannelByNameForTeamName", "1", "2", true).Return(nil, newAppError()) + + channel, err := client.Channel.GetByNameForTeamName("1", "2", true) + require.EqualError(t, err, "here: id, an error occurred") + require.Zero(t, channel) + }) +} + +func TestGetTeamUserChannels(t *testing.T) { + t.Run("success", func(t *testing.T) { + api := &plugintest.API{} + defer api.AssertExpectations(t) + client := pluginapi.NewClient(api, &plugintest.Driver{}) + + api.On("GetChannelsForTeamForUser", "1", "2", true).Return([]*model.Channel{{TeamId: "3"}, {TeamId: "4"}}, nil) + + channels, err := client.Channel.ListForTeamForUser("1", "2", true) + require.NoError(t, err) + require.Equal(t, []*model.Channel{{TeamId: "3"}, {TeamId: "4"}}, channels) + }) + + t.Run("failure", func(t *testing.T) { + api := &plugintest.API{} + defer api.AssertExpectations(t) + client := pluginapi.NewClient(api, &plugintest.Driver{}) + + appErr := model.NewAppError("here", "id", nil, "an error occurred", http.StatusInternalServerError) + + api.On("GetChannelsForTeamForUser", "1", "2", true).Return(nil, appErr) + + channels, err := client.Channel.ListForTeamForUser("1", "2", true) + require.Equal(t, appErr, err) + require.Len(t, channels, 0) + }) +} + +func TestGetPublicTeamChannels(t *testing.T) { + t.Run("success", func(t *testing.T) { + api := &plugintest.API{} + defer api.AssertExpectations(t) + client := pluginapi.NewClient(api, &plugintest.Driver{}) + + api.On("GetPublicChannelsForTeam", "1", 2, 3).Return([]*model.Channel{{TeamId: "3"}, {TeamId: "4"}}, nil) + + channels, err := client.Channel.ListPublicChannelsForTeam("1", 2, 3) + require.NoError(t, err) + require.Equal(t, []*model.Channel{{TeamId: "3"}, {TeamId: "4"}}, channels) + }) + + t.Run("failure", func(t *testing.T) { + api := &plugintest.API{} + defer api.AssertExpectations(t) + client := pluginapi.NewClient(api, &plugintest.Driver{}) + + appErr := model.NewAppError("here", "id", nil, "an error occurred", http.StatusInternalServerError) + + api.On("GetPublicChannelsForTeam", "1", 2, 3).Return(nil, appErr) + + channels, err := client.Channel.ListPublicChannelsForTeam("1", 2, 3) + require.Equal(t, appErr, err) + require.Len(t, channels, 0) + }) +} + +func TestCreateChannel(t *testing.T) { + t.Run("create channel with no replicas", func(t *testing.T) { + api := &plugintest.API{} + defer api.AssertExpectations(t) + client := pluginapi.NewClient(api, &plugintest.Driver{}) + + config := &model.Config{ + SqlSettings: model.SqlSettings{ + DataSourceReplicas: []string{}, + }, + } + api.On("GetConfig").Return(config).Once() + + c := &model.Channel{ + Id: model.NewId(), + Name: "name", + DisplayName: "displayname", + } + api.On("CreateChannel", c).Return(c, nil).Once() + + err := client.Channel.Create(c) + require.NoError(t, err) + }) + + t.Run("create channel and wait once", func(t *testing.T) { + api := &plugintest.API{} + defer api.AssertExpectations(t) + client := pluginapi.NewClient(api, &plugintest.Driver{}) + + config := &model.Config{ + SqlSettings: model.SqlSettings{ + DataSourceReplicas: []string{"replica1"}, + }, + } + api.On("GetConfig").Return(config).Once() + + c := &model.Channel{ + Id: model.NewId(), + Name: "name", + DisplayName: "displayname", + } + api.On("CreateChannel", c).Return(c, nil).Once() + api.On("GetChannel", c.Id).Return(c, nil).Once() + + err := client.Channel.Create(c) + require.NoError(t, err) + }) + + t.Run("create channel and wait multiple times", func(t *testing.T) { + api := &plugintest.API{} + defer api.AssertExpectations(t) + client := pluginapi.NewClient(api, &plugintest.Driver{}) + + config := &model.Config{ + SqlSettings: model.SqlSettings{ + DataSourceReplicas: []string{"replica1"}, + }, + } + api.On("GetConfig").Return(config).Once() + + c := &model.Channel{ + Id: model.NewId(), + Name: "name", + DisplayName: "displayname", + } + api.On("CreateChannel", c).Return(c, nil).Once() + + notFoundErr := model.NewAppError("", "", nil, "", http.StatusNotFound) + api.On("GetChannel", c.Id).Return(c, notFoundErr).Times(3) + api.On("GetChannel", c.Id).Return(c, nil).Times(1) + + err := client.Channel.Create(c) + require.NoError(t, err) + }) + + t.Run("create channel, wait multiple times and return error", func(t *testing.T) { + api := &plugintest.API{} + defer api.AssertExpectations(t) + client := pluginapi.NewClient(api, &plugintest.Driver{}) + + config := &model.Config{ + SqlSettings: model.SqlSettings{ + DataSourceReplicas: []string{"replica1"}, + }, + } + api.On("GetConfig").Return(config).Once() + + c := &model.Channel{ + Id: model.NewId(), + Name: "name", + DisplayName: "displayname", + } + api.On("CreateChannel", c).Return(c, nil).Once() + + notFoundErr := model.NewAppError("", "", nil, "", http.StatusNotFound) + api.On("GetChannel", c.Id).Return(c, notFoundErr).Times(3) + + otherErr := model.NewAppError("", "", nil, "", http.StatusInternalServerError) + api.On("GetChannel", c.Id).Return(c, otherErr).Times(1) + + err := client.Channel.Create(c) + require.Error(t, err) + }) + + t.Run("create channel, give up waiting", func(t *testing.T) { + api := &plugintest.API{} + defer api.AssertExpectations(t) + client := pluginapi.NewClient(api, &plugintest.Driver{}) + + config := &model.Config{ + SqlSettings: model.SqlSettings{ + DataSourceReplicas: []string{"replica1"}, + }, + } + api.On("GetConfig").Return(config).Once() + + c := &model.Channel{ + Id: model.NewId(), + Name: "name", + DisplayName: "displayname", + } + api.On("CreateChannel", c).Return(c, nil).Once() + + notFoundErr := model.NewAppError("", "", nil, "", http.StatusNotFound) + api.On("GetChannel", c.Id).Return(c, notFoundErr) + + err := client.Channel.Create(c) + require.Error(t, err) + require.Contains(t, err.Error(), "giving up waiting") + }) +} + +func TestCreateSidebarCategory(t *testing.T) { + t.Run("success", func(t *testing.T) { + api := &plugintest.API{} + defer api.AssertExpectations(t) + client := pluginapi.NewClient(api, &plugintest.Driver{}) + + category := model.SidebarCategoryWithChannels{} + + api.On("CreateChannelSidebarCategory", "user_id", "team_id", &category). + Return(&model.SidebarCategoryWithChannels{ + SidebarCategory: model.SidebarCategory{ + Id: "id", + UserId: "user_id", + TeamId: "team_id", + }, + Channels: []string{"channelA", "channelB"}}, + nil) + + err := client.Channel.CreateSidebarCategory("user_id", "team_id", &category) + + require.NoError(t, err) + require.Equal(t, + model.SidebarCategoryWithChannels{ + SidebarCategory: model.SidebarCategory{Id: "id", UserId: "user_id", TeamId: "team_id"}, + Channels: []string{"channelA", "channelB"}}, + category) + }) + + t.Run("failure", func(t *testing.T) { + api := &plugintest.API{} + defer api.AssertExpectations(t) + client := pluginapi.NewClient(api, &plugintest.Driver{}) + + inputCategory := model.SidebarCategoryWithChannels{} + appErr := model.NewAppError("here", "id", nil, "an error occurred", http.StatusInternalServerError) + + api.On("CreateChannelSidebarCategory", "user_id", "team_id", &inputCategory). + Return(&model.SidebarCategoryWithChannels{}, appErr) + + err := client.Channel.CreateSidebarCategory("user_id", "team_id", &inputCategory) + + require.Equal(t, appErr, err) + }) +} + +func TestGetSidebarCategories(t *testing.T) { + t.Run("success", func(t *testing.T) { + api := &plugintest.API{} + defer api.AssertExpectations(t) + client := pluginapi.NewClient(api, &plugintest.Driver{}) + + api.On("GetChannelSidebarCategories", "user_id", "team_id"). + Return(&model.OrderedSidebarCategories{ + Categories: nil, + Order: []string{"channelA", "channelB"}, + }, + nil) + + categories, err := client.Channel.GetSidebarCategories("user_id", "team_id") + + require.NoError(t, err) + require.Equal(t, + model.OrderedSidebarCategories{ + Categories: nil, + Order: []string{"channelA", "channelB"}, + }, + *categories) + }) + + t.Run("failure", func(t *testing.T) { + api := &plugintest.API{} + defer api.AssertExpectations(t) + client := pluginapi.NewClient(api, &plugintest.Driver{}) + + appErr := model.NewAppError("here", "id", nil, "an error occurred", http.StatusInternalServerError) + + api.On("GetChannelSidebarCategories", "user_id", "team_id").Return(nil, appErr) + + _, err := client.Channel.GetSidebarCategories("user_id", "team_id") + + require.Equal(t, appErr, err) + }) +} + +func TestUpdateSidebarCategories(t *testing.T) { + t.Run("success", func(t *testing.T) { + api := &plugintest.API{} + client := pluginapi.NewClient(api, &plugintest.Driver{}) + + categories := []*model.SidebarCategoryWithChannels{ + { + SidebarCategory: model.SidebarCategory{}, + Channels: nil, + }, + } + updatedCategories := []*model.SidebarCategoryWithChannels{ + { + SidebarCategory: model.SidebarCategory{ + Id: "id", + UserId: "user_id", + TeamId: "team_id", + }, + Channels: []string{"channelA", "channelB"}, + }} + + api.On("UpdateChannelSidebarCategories", "user_id", "team_id", categories).Return(updatedCategories, nil) + + err := client.Channel.UpdateSidebarCategories("user_id", "team_id", categories) + + require.NoError(t, err) + require.EqualValues(t, updatedCategories, categories) + }) + + t.Run("failure", func(t *testing.T) { + api := &plugintest.API{} + client := pluginapi.NewClient(api, &plugintest.Driver{}) + + inputCategories := []*model.SidebarCategoryWithChannels{ + { + SidebarCategory: model.SidebarCategory{}, + Channels: nil, + }, + } + appErr := model.NewAppError("here", "id", nil, "an error occurred", http.StatusInternalServerError) + + api.On("UpdateChannelSidebarCategories", "user_id", "team_id", inputCategories).Return(nil, appErr) + + err := client.Channel.UpdateSidebarCategories("user_id", "team_id", inputCategories) + + require.Equal(t, appErr, err) + }) +} diff --git a/server/public/pluginapi/client.go b/server/public/pluginapi/client.go new file mode 100644 index 00000000000..e917ecb8c7f --- /dev/null +++ b/server/public/pluginapi/client.go @@ -0,0 +1,79 @@ +package pluginapi + +import ( + "github.com/blang/semver/v4" + "github.com/mattermost/mattermost/server/public/plugin" + "github.com/pkg/errors" +) + +// Client is a streamlined wrapper over the mattermost plugin API. +type Client struct { + api plugin.API + + Bot BotService + Channel ChannelService + Cluster ClusterService + Configuration ConfigurationService + SlashCommand SlashCommandService + OAuth OAuthService + Emoji EmojiService + File FileService + Frontend FrontendService + Group GroupService + KV KVService + Log LogService + Mail MailService + Plugin PluginService + Post PostService + Session SessionService + Store *StoreService + System SystemService + Team TeamService + User UserService +} + +// NewClient creates a new instance of Client. +// +// This client must only be created once per plugin to +// prevent reacquiring of resources. +func NewClient(api plugin.API, driver plugin.Driver) *Client { + return &Client{ + api: api, + + Bot: BotService{api: api}, + Channel: ChannelService{api: api}, + Cluster: ClusterService{api: api}, + Configuration: ConfigurationService{api: api}, + SlashCommand: SlashCommandService{api: api}, + OAuth: OAuthService{api: api}, + Emoji: EmojiService{api: api}, + File: FileService{api: api}, + Frontend: FrontendService{api: api}, + Group: GroupService{api: api}, + KV: KVService{api: api}, + Log: LogService{api: api}, + Mail: MailService{api: api}, + Plugin: PluginService{api: api}, + Post: PostService{api: api}, + Session: SessionService{api: api}, + Store: &StoreService{ + api: api, + driver: driver, + }, + System: SystemService{api: api}, + Team: TeamService{api: api}, + User: UserService{api: api}, + } +} + +func ensureServerVersion(api plugin.API, required string) error { + serverVersion := api.GetServerVersion() + currentVersion := semver.MustParse(serverVersion) + requiredVersion := semver.MustParse(required) + + if currentVersion.LT(requiredVersion) { + return errors.Errorf("incompatible server version for plugin, minimum required version: %s, current version: %s", required, serverVersion) + } + + return nil +} diff --git a/server/public/pluginapi/cluster.go b/server/public/pluginapi/cluster.go new file mode 100644 index 00000000000..23af8d7a322 --- /dev/null +++ b/server/public/pluginapi/cluster.go @@ -0,0 +1,23 @@ +package pluginapi + +import ( + "github.com/mattermost/mattermost/server/public/model" + "github.com/mattermost/mattermost/server/public/plugin" +) + +// ClusterService exposes methods to interact with cluster nodes. +type ClusterService struct { + api plugin.API +} + +// ClusterService broadcasts a plugin event to all other running instances of +// the calling plugin that are present in the cluster. +// +// This method is used to allow plugin communication in a High-Availability cluster. +// The receiving side should implement the OnPluginClusterEvent hook +// to receive events sent through this method. +// +// Minimum server version: 5.36 +func (c *ClusterService) PublishPluginEvent(ev model.PluginClusterEvent, opts model.PluginClusterEventSendOptions) error { + return c.api.PublishPluginClusterEvent(ev, opts) +} diff --git a/server/public/pluginapi/cluster/doc.go b/server/public/pluginapi/cluster/doc.go new file mode 100644 index 00000000000..2024d02b274 --- /dev/null +++ b/server/public/pluginapi/cluster/doc.go @@ -0,0 +1,3 @@ +// package cluster exposes synchronization primitives to ensure correct behavior across multiple +// plugin instances in a Mattermost cluster. +package cluster diff --git a/server/public/pluginapi/cluster/job.go b/server/public/pluginapi/cluster/job.go new file mode 100644 index 00000000000..b4e43b29e27 --- /dev/null +++ b/server/public/pluginapi/cluster/job.go @@ -0,0 +1,229 @@ +package cluster + +import ( + "encoding/json" + "sync" + "time" + + "github.com/pkg/errors" + + "github.com/mattermost/mattermost/server/public/model" +) + +const ( + // cronPrefix is used to namespace key values created for a job from other key values + // created by a plugin. + cronPrefix = "cron_" +) + +// JobPluginAPI is the plugin API interface required to schedule jobs. +type JobPluginAPI interface { + MutexPluginAPI + KVGet(key string) ([]byte, *model.AppError) + KVDelete(key string) *model.AppError + KVList(page, count int) ([]string, *model.AppError) +} + +// JobConfig defines the configuration of a scheduled job. +type JobConfig struct { + // Interval is the period of execution for the job. + Interval time.Duration +} + +// NextWaitInterval is a callback computing the next wait interval for a job. +type NextWaitInterval func(now time.Time, metadata JobMetadata) time.Duration + +// MakeWaitForInterval creates a function to scheduling a job to run on the given interval relative +// to the last finished timestamp. +// +// For example, if the job first starts at 12:01 PM, and is configured with interval 5 minutes, +// it will next run at: +// +// 12:06, 12:11, 12:16, ... +// +// If the job has not previously started, it will run immediately. +func MakeWaitForInterval(interval time.Duration) NextWaitInterval { + if interval == 0 { + panic("must specify non-zero ready interval") + } + + return func(now time.Time, metadata JobMetadata) time.Duration { + sinceLastFinished := now.Sub(metadata.LastFinished) + if sinceLastFinished < interval { + return interval - sinceLastFinished + } + + return 0 + } +} + +// MakeWaitForRoundedInterval creates a function, scheduling a job to run on the nearest rounded +// interval relative to the last finished timestamp. +// +// For example, if the job first starts at 12:04 PM, and is configured with interval 5 minutes, +// and is configured to round to 5 minute intervals, it will next run at: +// +// 12:05 PM, 12:10 PM, 12:15 PM, ... +// +// If the job has not previously started, it will run immediately. Note that this wait interval +// strategy does not guarantee a minimum interval between runs, only that subsequent runs will be +// scheduled on the rounded interval. +func MakeWaitForRoundedInterval(interval time.Duration) NextWaitInterval { + if interval == 0 { + panic("must specify non-zero ready interval") + } + + return func(now time.Time, metadata JobMetadata) time.Duration { + if metadata.LastFinished.IsZero() { + return 0 + } + + target := metadata.LastFinished.Add(interval).Truncate(interval) + untilTarget := target.Sub(now) + if untilTarget > 0 { + return untilTarget + } + + return 0 + } +} + +// Job is a scheduled job whose callback function is executed on a configured interval by at most +// one plugin instance at a time. +// +// Use scheduled jobs to perform background activity on a regular interval without having to +// explicitly coordinate with other instances of the same plugin that might repeat that effort. +type Job struct { + pluginAPI JobPluginAPI + key string + mutex *Mutex + nextWaitInterval NextWaitInterval + callback func() + + stopOnce sync.Once + stop chan bool + done chan bool +} + +// JobMetadata persists metadata about job execution. +type JobMetadata struct { + // LastFinished is the last time the job finished anywhere in the cluster. + LastFinished time.Time +} + +// Schedule creates a scheduled job. +func Schedule(pluginAPI JobPluginAPI, key string, nextWaitInterval NextWaitInterval, callback func()) (*Job, error) { + key = cronPrefix + key + + mutex, err := NewMutex(pluginAPI, key) + if err != nil { + return nil, errors.Wrap(err, "failed to create job mutex") + } + + job := &Job{ + pluginAPI: pluginAPI, + key: key, + mutex: mutex, + nextWaitInterval: nextWaitInterval, + callback: callback, + stop: make(chan bool), + done: make(chan bool), + } + + go job.run() + + return job, nil +} + +// readMetadata reads the job execution metadata from the kv store. +func (j *Job) readMetadata() (JobMetadata, error) { + data, appErr := j.pluginAPI.KVGet(j.key) + if appErr != nil { + return JobMetadata{}, errors.Wrap(appErr, "failed to read data") + } + + if data == nil { + return JobMetadata{}, nil + } + + var metadata JobMetadata + err := json.Unmarshal(data, &metadata) + if err != nil { + return JobMetadata{}, errors.Wrap(err, "failed to decode data") + } + + return metadata, nil +} + +// saveMetadata writes updated job execution metadata from the kv store. +// +// It is assumed that the job mutex is held, negating the need to require an atomic write. +func (j *Job) saveMetadata(metadata JobMetadata) error { + data, err := json.Marshal(metadata) + if err != nil { + return errors.Wrap(err, "failed to marshal data") + } + + ok, appErr := j.pluginAPI.KVSetWithOptions(j.key, data, model.PluginKVSetOptions{}) + if appErr != nil || !ok { + return errors.Wrap(appErr, "failed to set data") + } + + return nil +} + +// run attempts to run the scheduled job, guaranteeing only one instance is executing concurrently. +func (j *Job) run() { + defer close(j.done) + + var waitInterval time.Duration + + for { + select { + case <-j.stop: + return + case <-time.After(waitInterval): + } + + func() { + // Acquire the corresponding job lock and hold it throughout execution. + j.mutex.Lock() + defer j.mutex.Unlock() + + metadata, err := j.readMetadata() + if err != nil { + j.pluginAPI.LogError("failed to read job metadata", "err", err, "key", j.key) + waitInterval = nextWaitInterval(waitInterval, err) + return + } + + // Is it time to run the job? + waitInterval = j.nextWaitInterval(time.Now(), metadata) + if waitInterval > 0 { + return + } + + // Run the job + j.callback() + + metadata.LastFinished = time.Now() + + err = j.saveMetadata(metadata) + if err != nil { + j.pluginAPI.LogError("failed to write job data", "err", err, "key", j.key) + } + + waitInterval = j.nextWaitInterval(time.Now(), metadata) + }() + } +} + +// Close terminates a scheduled job, preventing it from being scheduled on this plugin instance. +func (j *Job) Close() error { + j.stopOnce.Do(func() { + close(j.stop) + }) + <-j.done + + return nil +} diff --git a/server/public/pluginapi/cluster/job_example_test.go b/server/public/pluginapi/cluster/job_example_test.go new file mode 100644 index 00000000000..3d5235c39d9 --- /dev/null +++ b/server/public/pluginapi/cluster/job_example_test.go @@ -0,0 +1,25 @@ +package cluster + +import ( + "time" + + "github.com/mattermost/mattermost/server/public/plugin" +) + +func ExampleSchedule() { + // Use p.API from your plugin instead. + pluginAPI := plugin.API(nil) + + callback := func() { + // periodic work to do + } + + job, err := Schedule(pluginAPI, "key", MakeWaitForInterval(5*time.Minute), callback) + if err != nil { + panic("failed to schedule job") + } + + // main thread + + defer job.Close() +} diff --git a/server/public/pluginapi/cluster/job_once.go b/server/public/pluginapi/cluster/job_once.go new file mode 100644 index 00000000000..7b05bd866ad --- /dev/null +++ b/server/public/pluginapi/cluster/job_once.go @@ -0,0 +1,235 @@ +package cluster + +import ( + "encoding/json" + "math/rand" + "sync" + "time" + + "github.com/pkg/errors" + + "github.com/mattermost/mattermost/server/public/model" +) + +const ( + // oncePrefix is used to namespace key values created for a scheduleOnce job + oncePrefix = "once_" + + // keysPerPage is the maximum number of keys to retrieve from the db per call + keysPerPage = 1000 + + // maxNumFails is the maximum number of KVStore read fails or failed attempts to run the + // callback until the scheduler cancels a job. + maxNumFails = 3 + + // waitAfterFail is the amount of time to wait after a failure + waitAfterFail = 1 * time.Second + + // pollNewJobsInterval is the amount of time to wait between polling the db for new scheduled jobs + pollNewJobsInterval = 5 * time.Minute + + // scheduleOnceJitter is the range of jitter to add to intervals to avoid contention issues + scheduleOnceJitter = 100 * time.Millisecond + + // propsLimit is the maximum length in bytes of the json-representation of a job's props. + // It exists to prevent job go rountines from consuming too much memory, as they are long running. + propsLimit = 10000 +) + +type JobOnceMetadata struct { + Key string + RunAt time.Time + Props any +} + +type JobOnce struct { + pluginAPI JobPluginAPI + clusterMutex *Mutex + + // key is the original key. It is prefixed with oncePrefix when used as a key in the KVStore + key string + props any + runAt time.Time + numFails int + + // done signals the job.run go routine to exit + done chan bool + doneOnce sync.Once + + // join is a join point for the job.run() goroutine to join the calling goroutine (in this case, + // the one calling job.Cancel) + join chan bool + joinOnce sync.Once + + storedCallback *syncedCallback + activeJobs *syncedJobs +} + +// Cancel terminates a scheduled job, preventing it from being scheduled on this plugin instance. +// It also removes the job from the db, preventing it from being run in the future. +func (j *JobOnce) Cancel() { + j.clusterMutex.Lock() + defer j.clusterMutex.Unlock() + + j.cancelWhileHoldingMutex() + + // join the running goroutine + j.joinOnce.Do(func() { + <-j.join + }) +} + +func newJobOnce(pluginAPI JobPluginAPI, key string, runAt time.Time, callback *syncedCallback, jobs *syncedJobs, props any) (*JobOnce, error) { + mutex, err := NewMutex(pluginAPI, key) + if err != nil { + return nil, errors.Wrap(err, "failed to create job mutex") + } + + propsBytes, err := json.Marshal(props) + if err != nil { + return nil, errors.Wrap(err, "failed to marshal props") + } + + if len(propsBytes) > propsLimit { + return nil, errors.Errorf("props length extends limit") + } + + return &JobOnce{ + pluginAPI: pluginAPI, + clusterMutex: mutex, + key: key, + props: props, + runAt: runAt, + done: make(chan bool), + join: make(chan bool), + storedCallback: callback, + activeJobs: jobs, + }, nil +} + +func (j *JobOnce) run() { + defer close(j.join) + + wait := time.Until(j.runAt) + + for { + select { + case <-j.done: + return + case <-time.After(wait + addJitter()): + } + + func() { + // Acquire the cluster mutex while we're trying to do the job + j.clusterMutex.Lock() + defer j.clusterMutex.Unlock() + + // Check that the job has not been completed + metadata, err := readMetadata(j.pluginAPI, j.key) + if err != nil { + j.numFails++ + if j.numFails > maxNumFails { + j.cancelWhileHoldingMutex() + return + } + + // wait a bit of time and try again + wait = waitAfterFail + return + } + + // If key doesn't exist, or if the runAt has changed, the original job has been completed already + if metadata == nil || !j.runAt.Equal(metadata.RunAt) { + j.cancelWhileHoldingMutex() + return + } + + j.executeJob() + + j.cancelWhileHoldingMutex() + }() + } +} + +func (j *JobOnce) executeJob() { + j.storedCallback.mu.Lock() + defer j.storedCallback.mu.Unlock() + + j.storedCallback.callback(j.key, j.props) +} + +// readMetadata reads the job's stored metadata. If the caller wishes to make an atomic +// read/write, the cluster mutex for job's key should be held. +func readMetadata(pluginAPI JobPluginAPI, key string) (*JobOnceMetadata, error) { + data, appErr := pluginAPI.KVGet(oncePrefix + key) + if appErr != nil { + return nil, errors.Wrap(normalizeAppErr(appErr), "failed to read data") + } + + if data == nil { + return nil, nil + } + + var metadata JobOnceMetadata + if err := json.Unmarshal(data, &metadata); err != nil { + return nil, errors.Wrap(err, "failed to decode data") + } + + return &metadata, nil +} + +// saveMetadata writes the job's metadata to the kvstore. saveMetadata acquires the job's cluster lock. +// saveMetadata will not overwrite an existing key. +func (j *JobOnce) saveMetadata() error { + j.clusterMutex.Lock() + defer j.clusterMutex.Unlock() + + metadata := JobOnceMetadata{ + Key: j.key, + Props: j.props, + RunAt: j.runAt, + } + data, err := json.Marshal(metadata) + if err != nil { + return errors.Wrap(err, "failed to marshal data") + } + + ok, appErr := j.pluginAPI.KVSetWithOptions(oncePrefix+j.key, data, model.PluginKVSetOptions{ + Atomic: true, + OldValue: nil, + }) + if appErr != nil { + return normalizeAppErr(appErr) + } + if !ok { + return errors.New("failed to set data") + } + + return nil +} + +// cancelWhileHoldingMutex assumes the caller holds the job's mutex. +func (j *JobOnce) cancelWhileHoldingMutex() { + // remove the job from the kv store, if it exists + _ = j.pluginAPI.KVDelete(oncePrefix + j.key) + + j.activeJobs.mu.Lock() + defer j.activeJobs.mu.Unlock() + delete(j.activeJobs.jobs, j.key) + + j.doneOnce.Do(func() { + close(j.done) + }) +} + +func addJitter() time.Duration { + return time.Duration(rand.Int63n(int64(scheduleOnceJitter))) +} + +func normalizeAppErr(appErr *model.AppError) error { + if appErr == nil { + return nil + } + + return appErr +} diff --git a/server/public/pluginapi/cluster/job_once_example_test.go b/server/public/pluginapi/cluster/job_once_example_test.go new file mode 100644 index 00000000000..cd058bf62a0 --- /dev/null +++ b/server/public/pluginapi/cluster/job_once_example_test.go @@ -0,0 +1,45 @@ +package cluster + +import ( + "log" + "time" + + "github.com/mattermost/mattermost/server/public/plugin" +) + +func HandleJobOnceCalls(key string, props any) { + if key == "the key i'm watching for" { + log.Println(props) + // Work to do only once per cluster + } +} + +func ExampleJobOnceScheduler_ScheduleOnce() { + // Use p.API from your plugin instead. + pluginAPI := plugin.API(nil) + + // Get the scheduler, which you can pass throughout the plugin... + scheduler := GetJobOnceScheduler(pluginAPI) + + // Set the plugin's callback handler + _ = scheduler.SetCallback(HandleJobOnceCalls) + + // Now start the scheduler, which starts the poller and schedules all waiting jobs. + _ = scheduler.Start() + + // main thread... + + // add a job + _, _ = scheduler.ScheduleOnce("the key i'm watching for", time.Now().Add(2*time.Hour), struct{ foo string }{"aasd"}) + + // Maybe you want to check the scheduled jobs, or cancel them. This is completely optional--there + // is no need to cancel jobs, even if you are shutting down. Call Cancel only when you want to + // cancel a future job. Cancelling a job will prevent it from running in the future on this or + // any server. + jobs, _ := scheduler.ListScheduledJobs() + defer func() { + for _, j := range jobs { + scheduler.Cancel(j.Key) + } + }() +} diff --git a/server/public/pluginapi/cluster/job_once_mem_test.go b/server/public/pluginapi/cluster/job_once_mem_test.go new file mode 100644 index 00000000000..2827ea26cdd --- /dev/null +++ b/server/public/pluginapi/cluster/job_once_mem_test.go @@ -0,0 +1,83 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package cluster + +import ( + "fmt" + "runtime" + "sync/atomic" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/mattermost/mattermost/server/public/model" +) + +func TestMemFootprint(t *testing.T) { + var memConsumed = func() uint64 { + runtime.GC() + var s runtime.MemStats + runtime.ReadMemStats(&s) + return s.Sys + } + + t.Run("average k per jobOnce", func(t *testing.T) { + t.SkipNow() + + makeKey := model.NewId + + numJobs := 100000 + jobs := make(map[string]*int32, numJobs) + for i := 0; i < numJobs; i++ { + jobs[makeKey()] = new(int32) + } + + callback := func(key string, _ any) { + count, ok := jobs[key] + if ok { + atomic.AddInt32(count, 1) + } + } + + mockPluginAPI := newMockPluginAPI(t) + s := GetJobOnceScheduler(mockPluginAPI) + err := s.SetCallback(callback) + require.NoError(t, err) + err = s.Start() + require.NoError(t, err) + + getVal := func(key string) []byte { + data, _ := s.pluginAPI.KVGet(key) + return data + } + + before := memConsumed() + + for k := range jobs { + assert.Empty(t, getVal(oncePrefix+k)) + _, err = s.ScheduleOnce(k, time.Now().Add(5*time.Minute), nil) + require.NoError(t, err) + assert.NotEmpty(t, getVal(oncePrefix+k)) + } + + time.Sleep(10 * time.Second) + + // Everything scheduled now: + s.activeJobs.mu.RLock() + assert.Equal(t, numJobs, len(s.activeJobs.jobs)) + s.activeJobs.mu.RUnlock() + list, err := s.ListScheduledJobs() + require.NoError(t, err) + assert.Equal(t, numJobs, len(list)) + + after := memConsumed() + + fmt.Printf("\nthe %d jobs, scheduler, and goroutines require: %.2fmB memory, or %.3fkB each job\n", + numJobs, + float64(after-before)/(1024*1024), + (float64(after-before)/float64(numJobs))/1024) + }) +} diff --git a/server/public/pluginapi/cluster/job_once_scheduler.go b/server/public/pluginapi/cluster/job_once_scheduler.go new file mode 100644 index 00000000000..ca314397ff9 --- /dev/null +++ b/server/public/pluginapi/cluster/job_once_scheduler.go @@ -0,0 +1,236 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package cluster + +import ( + "strings" + "sync" + "time" + + "github.com/pkg/errors" +) + +// syncedCallback uses the mutex to make things predictable for the client: the callback will be +// called once at a time (the client does not need to worry about concurrency within the callback) +type syncedCallback struct { + mu sync.Mutex + callback func(string, any) +} + +type syncedJobs struct { + mu sync.RWMutex + jobs map[string]*JobOnce +} + +type JobOnceScheduler struct { + pluginAPI JobPluginAPI + + startedMu sync.RWMutex + started bool + + activeJobs *syncedJobs + storedCallback *syncedCallback +} + +var schedulerOnce sync.Once +var s *JobOnceScheduler + +// GetJobOnceScheduler returns a scheduler which is ready to have its callback set. Repeated +// calls will return the same scheduler. +func GetJobOnceScheduler(pluginAPI JobPluginAPI) *JobOnceScheduler { + schedulerOnce.Do(func() { + s = &JobOnceScheduler{ + pluginAPI: pluginAPI, + activeJobs: &syncedJobs{ + jobs: make(map[string]*JobOnce), + }, + storedCallback: &syncedCallback{}, + } + }) + return s +} + +// Start starts the Scheduler. It finds all previous ScheduleOnce jobs and starts them running, and +// fires any jobs that have reached or exceeded their runAt time. Thus, even if a cluster goes down +// and is restarted, Start will restart previously scheduled jobs. +func (s *JobOnceScheduler) Start() error { + s.startedMu.Lock() + defer s.startedMu.Unlock() + if s.started { + return errors.New("scheduler has already been started") + } + + if err := s.verifyCallbackExists(); err != nil { + return errors.Wrap(err, "callback not found; cannot start scheduler") + } + + if err := s.scheduleNewJobsFromDB(); err != nil { + return errors.Wrap(err, "could not start JobOnceScheduler due to error") + } + + go s.pollForNewScheduledJobs() + + s.started = true + + return nil +} + +// SetCallback sets the scheduler's callback. When a job fires, the callback will be called with +// the job's id. +func (s *JobOnceScheduler) SetCallback(callback func(string, any)) error { + if callback == nil { + return errors.New("callback cannot be nil") + } + + s.storedCallback.mu.Lock() + defer s.storedCallback.mu.Unlock() + + s.storedCallback.callback = callback + return nil +} + +// ListScheduledJobs returns a list of the jobs in the db that have been scheduled. There is no +// guarantee that list is accurate by the time the caller reads the list. E.g., the jobs in the list +// may have been run, canceled, or new jobs may have scheduled. +func (s *JobOnceScheduler) ListScheduledJobs() ([]JobOnceMetadata, error) { + var ret []JobOnceMetadata + for i := 0; ; i++ { + keys, err := s.pluginAPI.KVList(i, keysPerPage) + if err != nil { + return nil, errors.Wrap(err, "error getting KVList") + } + for _, k := range keys { + if strings.HasPrefix(k, oncePrefix) { + metadata, err := readMetadata(s.pluginAPI, k[len(oncePrefix):]) + if err != nil { + s.pluginAPI.LogError(errors.Wrap(err, "could not retrieve data from plugin kvstore for key: "+k).Error()) + continue + } + if metadata == nil { + continue + } + + ret = append(ret, *metadata) + } + } + + if len(keys) < keysPerPage { + break + } + } + + return ret, nil +} + +// ScheduleOnce creates a scheduled job that will run once. When the clock reaches runAt, the +// callback will be called with key and props as the argument. +// +// If the job key already exists in the db, this will return an error. To reschedule a job, first +// cancel the original then schedule it again. +func (s *JobOnceScheduler) ScheduleOnce(key string, runAt time.Time, props any) (*JobOnce, error) { + s.startedMu.RLock() + defer s.startedMu.RUnlock() + if !s.started { + return nil, errors.New("start the scheduler before adding jobs") + } + + job, err := newJobOnce(s.pluginAPI, key, runAt, s.storedCallback, s.activeJobs, props) + if err != nil { + return nil, errors.Wrap(err, "could not create new job") + } + + if err = job.saveMetadata(); err != nil { + return nil, errors.Wrap(err, "could not save job metadata") + } + + s.runAndTrack(job) + + return job, nil +} + +// Cancel cancels a job by its key. This is useful if the plugin lost the original *JobOnce, or +// is stopping a job found in ListScheduledJobs(). +func (s *JobOnceScheduler) Cancel(key string) { + // using an anonymous function because job.Close() below needs access to the activeJobs mutex + job := func() *JobOnce { + s.activeJobs.mu.RLock() + defer s.activeJobs.mu.RUnlock() + j, ok := s.activeJobs.jobs[key] + if ok { + return j + } + + // Job wasn't active, so no need to call CancelWhileHoldingMutex (which shuts down the + // goroutine). There's a condition where another server in the cluster started the job, and + // the current server hasn't polled for it yet. To solve that case, delete it from the db. + mutex, err := NewMutex(s.pluginAPI, key) + if err != nil { + s.pluginAPI.LogError(errors.Wrap(err, "failed to create job mutex in Cancel for key: "+key).Error()) + } + mutex.Lock() + defer mutex.Unlock() + + _ = s.pluginAPI.KVDelete(oncePrefix + key) + + return nil + }() + + if job != nil { + job.Cancel() + } +} + +func (s *JobOnceScheduler) scheduleNewJobsFromDB() error { + scheduled, err := s.ListScheduledJobs() + if err != nil { + return errors.Wrap(err, "could not read scheduled jobs from db") + } + + for _, m := range scheduled { + job, err := newJobOnce(s.pluginAPI, m.Key, m.RunAt, s.storedCallback, s.activeJobs, m.Props) + if err != nil { + s.pluginAPI.LogError(errors.Wrap(err, "could not create new job for key: "+m.Key).Error()) + continue + } + + s.runAndTrack(job) + } + + return nil +} + +func (s *JobOnceScheduler) runAndTrack(job *JobOnce) { + s.activeJobs.mu.Lock() + defer s.activeJobs.mu.Unlock() + + // has this been scheduled already on this server? + if _, ok := s.activeJobs.jobs[job.key]; ok { + return + } + + go job.run() + + s.activeJobs.jobs[job.key] = job +} + +// pollForNewScheduledJobs will only be started once per plugin. It doesn't need to be stopped. +func (s *JobOnceScheduler) pollForNewScheduledJobs() { + for { + <-time.After(pollNewJobsInterval + addJitter()) + + if err := s.scheduleNewJobsFromDB(); err != nil { + s.pluginAPI.LogError("pluginAPI scheduleOnce poller encountered an error but is still polling", "error", err) + } + } +} + +func (s *JobOnceScheduler) verifyCallbackExists() error { + s.storedCallback.mu.Lock() + defer s.storedCallback.mu.Unlock() + + if s.storedCallback.callback == nil { + return errors.New("set callback before starting the scheduler") + } + return nil +} diff --git a/server/public/pluginapi/cluster/job_once_test.go b/server/public/pluginapi/cluster/job_once_test.go new file mode 100644 index 00000000000..85a2d0f8f8e --- /dev/null +++ b/server/public/pluginapi/cluster/job_once_test.go @@ -0,0 +1,685 @@ +package cluster + +import ( + "encoding/json" + "sync" + "sync/atomic" + "testing" + "time" + + "github.com/mattermost/mattermost/server/public/model" + "github.com/pkg/errors" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestScheduleOnceParallel(t *testing.T) { + makeKey := model.NewId + + // there is only one callback by design, so all tests need to add their key + // and callback handling code here. + jobKey1 := makeKey() + count1 := new(int32) + jobKey2 := makeKey() + count2 := new(int32) + jobKey3 := makeKey() + jobKey4 := makeKey() + count4 := new(int32) + jobKey5 := makeKey() + count5 := new(int32) + + manyJobs := make(map[string]*int32) + for i := 0; i < 100; i++ { + manyJobs[makeKey()] = new(int32) + } + + callback := func(key string, _ any) { + switch key { + case jobKey1: + atomic.AddInt32(count1, 1) + case jobKey2: + atomic.AddInt32(count2, 1) + case jobKey3: + return // do nothing, like an error occurred in the plugin + case jobKey4: + atomic.AddInt32(count4, 1) + case jobKey5: + atomic.AddInt32(count5, 1) + default: + count, ok := manyJobs[key] + if ok { + atomic.AddInt32(count, 1) + return + } + } + } + + mockPluginAPI := newMockPluginAPI(t) + getVal := func(key string) []byte { + data, _ := mockPluginAPI.KVGet(key) + return data + } + + s := GetJobOnceScheduler(mockPluginAPI) + + // should error if we try to start without callback + err := s.Start() + require.Error(t, err) + + err = s.SetCallback(callback) + require.NoError(t, err) + err = s.Start() + require.NoError(t, err) + + jobs, err := s.ListScheduledJobs() + require.NoError(t, err) + require.Empty(t, jobs) + + t.Run("one scheduled job", func(t *testing.T) { + t.Parallel() + + job, err2 := s.ScheduleOnce(jobKey1, time.Now().Add(100*time.Millisecond), nil) + require.NoError(t, err2) + require.NotNil(t, job) + assert.NotEmpty(t, getVal(oncePrefix+jobKey1)) + + time.Sleep(200*time.Millisecond + scheduleOnceJitter) + + assert.Empty(t, getVal(oncePrefix+jobKey1)) + s.activeJobs.mu.RLock() + assert.Empty(t, s.activeJobs.jobs[jobKey1]) + s.activeJobs.mu.RUnlock() + + // It's okay to cancel jobs extra times, even if they're completed. + job.Cancel() + job.Cancel() + job.Cancel() + job.Cancel() + + // Should have been called once + assert.Equal(t, int32(1), atomic.LoadInt32(count1)) + }) + + t.Run("one job, stopped before firing", func(t *testing.T) { + t.Parallel() + + job, err2 := s.ScheduleOnce(jobKey2, time.Now().Add(100*time.Millisecond), nil) + require.NoError(t, err2) + require.NotNil(t, job) + assert.NotEmpty(t, getVal(oncePrefix+jobKey2)) + + job.Cancel() + assert.Empty(t, getVal(oncePrefix+jobKey2)) + s.activeJobs.mu.RLock() + assert.Empty(t, s.activeJobs.jobs[jobKey2]) + s.activeJobs.mu.RUnlock() + + time.Sleep(2 * (waitAfterFail + scheduleOnceJitter)) + + // Should not have been called + assert.Equal(t, int32(0), atomic.LoadInt32(count2)) + + // It's okay to cancel jobs extra times, even if they're completed. + job.Cancel() + job.Cancel() + job.Cancel() + job.Cancel() + }) + + t.Run("failed at the plugin, job removed from db", func(t *testing.T) { + t.Parallel() + + job, err2 := s.ScheduleOnce(jobKey3, time.Now().Add(100*time.Millisecond), nil) + require.NoError(t, err2) + require.NotNil(t, job) + assert.NotEmpty(t, getVal(oncePrefix+jobKey3)) + + time.Sleep(200*time.Millisecond + scheduleOnceJitter) + assert.Empty(t, getVal(oncePrefix+jobKey3)) + s.activeJobs.mu.RLock() + assert.Empty(t, s.activeJobs.jobs[jobKey3]) + s.activeJobs.mu.RUnlock() + }) + + t.Run("cancel and restart a job with the same key", func(t *testing.T) { + t.Parallel() + + job, err2 := s.ScheduleOnce(jobKey4, time.Now().Add(100*time.Millisecond), nil) + require.NoError(t, err2) + require.NotNil(t, job) + assert.NotEmpty(t, getVal(oncePrefix+jobKey4)) + + job.Cancel() + assert.Empty(t, getVal(oncePrefix+jobKey4)) + s.activeJobs.mu.RLock() + assert.Empty(t, s.activeJobs.jobs[jobKey4]) + s.activeJobs.mu.RUnlock() + + job, err2 = s.ScheduleOnce(jobKey4, time.Now().Add(100*time.Millisecond), nil) + require.NoError(t, err2) + require.NotNil(t, job) + assert.NotEmpty(t, getVal(oncePrefix+jobKey4)) + + time.Sleep(200*time.Millisecond + scheduleOnceJitter) + assert.Equal(t, int32(1), atomic.LoadInt32(count4)) + assert.Empty(t, getVal(oncePrefix+jobKey4)) + s.activeJobs.mu.RLock() + assert.Empty(t, s.activeJobs.jobs[jobKey4]) + s.activeJobs.mu.RUnlock() + }) + + t.Run("many scheduled jobs", func(t *testing.T) { + t.Parallel() + + for k := range manyJobs { + job, err2 := s.ScheduleOnce(k, time.Now().Add(100*time.Millisecond), nil) + require.NoError(t, err2) + require.NotNil(t, job) + assert.NotEmpty(t, getVal(oncePrefix+k)) + } + + time.Sleep(200*time.Millisecond + scheduleOnceJitter) + + for k, v := range manyJobs { + assert.Empty(t, getVal(oncePrefix+k)) + s.activeJobs.mu.RLock() + assert.Empty(t, s.activeJobs.jobs[k]) + s.activeJobs.mu.RUnlock() + assert.Equal(t, int32(1), *v) + } + }) + + t.Run("cancel a job by key name", func(t *testing.T) { + t.Parallel() + + job, err2 := s.ScheduleOnce(jobKey5, time.Now().Add(100*time.Millisecond), nil) + require.NoError(t, err2) + require.NotNil(t, job) + assert.NotEmpty(t, getVal(oncePrefix+jobKey5)) + s.activeJobs.mu.RLock() + assert.NotEmpty(t, s.activeJobs.jobs[jobKey5]) + s.activeJobs.mu.RUnlock() + + s.Cancel(jobKey5) + + assert.Empty(t, getVal(oncePrefix+jobKey5)) + s.activeJobs.mu.RLock() + assert.Empty(t, s.activeJobs.jobs[jobKey5]) + s.activeJobs.mu.RUnlock() + + // cancel it again doesn't do anything: + s.Cancel(jobKey5) + + time.Sleep(150*time.Millisecond + scheduleOnceJitter) + assert.Equal(t, int32(0), atomic.LoadInt32(count5)) + }) + + t.Run("starting the scheduler again will return an error", func(t *testing.T) { + t.Parallel() + + newScheduler := GetJobOnceScheduler(mockPluginAPI) + err = newScheduler.Start() + require.Error(t, err) + }) +} + +func TestScheduleOnceSequential(t *testing.T) { + makeKey := model.NewId + + // get the existing scheduler + s := GetJobOnceScheduler(newMockPluginAPI(t)) + getVal := func(key string) []byte { + data, _ := s.pluginAPI.KVGet(key) + return data + } + setMetadata := func(key string, metadata JobOnceMetadata) error { + data, err := json.Marshal(metadata) + if err != nil { + return err + } + ok, appErr := s.pluginAPI.KVSetWithOptions(oncePrefix+key, data, model.PluginKVSetOptions{}) + if !ok { + return errors.New("KVSetWithOptions failed") + } + if appErr != nil { + return normalizeAppErr(appErr) + } + return nil + } + + resetScheduler := func() { + s.activeJobs.mu.Lock() + defer s.activeJobs.mu.Unlock() + s.activeJobs.jobs = make(map[string]*JobOnce) + s.storedCallback.mu.Lock() + defer s.storedCallback.mu.Unlock() + s.storedCallback.callback = nil + s.startedMu.Lock() + defer s.startedMu.Unlock() + s.started = false + s.pluginAPI.(*mockPluginAPI).clear() + } + + t.Run("starting the scheduler without a callback will return an error", func(t *testing.T) { + resetScheduler() + + err := s.Start() + require.Error(t, err) + }) + + t.Run("trying to schedule a job without starting will return an error", func(t *testing.T) { + resetScheduler() + + callback := func(key string, _ any) {} + err := s.SetCallback(callback) + require.NoError(t, err) + + _, err = s.ScheduleOnce("will fail", time.Now(), nil) + require.Error(t, err) + }) + + t.Run("adding two callback works, only second one is called", func(t *testing.T) { + resetScheduler() + + newCount2 := new(int32) + newCount3 := new(int32) + + callback2 := func(key string, _ any) { + atomic.AddInt32(newCount2, 1) + } + callback3 := func(key string, _ any) { + atomic.AddInt32(newCount3, 1) + } + + err := s.SetCallback(callback2) + require.NoError(t, err) + err = s.SetCallback(callback3) + require.NoError(t, err) + err = s.Start() + require.NoError(t, err) + + _, err = s.ScheduleOnce("anything", time.Now().Add(50*time.Millisecond), nil) + require.NoError(t, err) + time.Sleep(70*time.Millisecond + scheduleOnceJitter) + assert.Equal(t, int32(0), atomic.LoadInt32(newCount2)) + assert.Equal(t, int32(1), atomic.LoadInt32(newCount3)) + }) + + t.Run("test paging keys from the db by inserting 3 pages of jobs and starting scheduler", func(t *testing.T) { + resetScheduler() + + numPagingJobs := keysPerPage*3 + 2 + testPagingJobs := make(map[string]*int32) + for i := 0; i < numPagingJobs; i++ { + testPagingJobs[makeKey()] = new(int32) + } + + callback := func(key string, _ any) { + count, ok := testPagingJobs[key] + if ok { + atomic.AddInt32(count, 1) + return + } + } + + // add the test paging jobs before starting scheduler + for k := range testPagingJobs { + assert.Empty(t, getVal(oncePrefix+k)) + job, err := newJobOnce(s.pluginAPI, k, time.Now().Add(100*time.Millisecond), s.storedCallback, s.activeJobs, nil) + require.NoError(t, err) + err = job.saveMetadata() + require.NoError(t, err) + assert.NotEmpty(t, getVal(oncePrefix+k)) + } + + jobs, err := s.ListScheduledJobs() + require.NoError(t, err) + assert.Equal(t, len(testPagingJobs), len(jobs)) + + err = s.SetCallback(callback) + require.NoError(t, err) + + // reschedule from the db: + err = s.scheduleNewJobsFromDB() + require.NoError(t, err) + + // wait for the testPagingJobs created in the setup to finish + time.Sleep(300 * time.Millisecond) + + numInDB := 0 + numActive := 0 + numCountsAtZero := 0 + for k, v := range testPagingJobs { + if getVal(oncePrefix+k) != nil { + numInDB++ + } + s.activeJobs.mu.RLock() + if s.activeJobs.jobs[k] != nil { + numActive++ + } + s.activeJobs.mu.RUnlock() + if atomic.LoadInt32(v) == int32(0) { + numCountsAtZero++ + } + } + + assert.Equal(t, 0, numInDB) + assert.Equal(t, 0, numActive) + assert.Equal(t, 0, numCountsAtZero) + }) + + t.Run("failed at the db", func(t *testing.T) { + resetScheduler() + + jobKey1 := makeKey() + count1 := new(int32) + + callback := func(key string, _ any) { + if key == jobKey1 { + atomic.AddInt32(count1, 1) + } + } + + err := s.SetCallback(callback) + require.NoError(t, err) + err = s.Start() + require.NoError(t, err) + + jobs, err := s.ListScheduledJobs() + require.NoError(t, err) + require.Empty(t, jobs) + + job, err := s.ScheduleOnce(jobKey1, time.Now().Add(100*time.Millisecond), nil) + require.NoError(t, err) + require.NotNil(t, job) + assert.NotEmpty(t, getVal(oncePrefix+jobKey1)) + assert.NotEmpty(t, s.activeJobs.jobs[jobKey1]) + s.pluginAPI.(*mockPluginAPI).setFailingWithPrefix(oncePrefix) + + // wait until the metadata has failed to read + time.Sleep((maxNumFails + 1) * (waitAfterFail + scheduleOnceJitter)) + assert.Equal(t, int32(0), atomic.LoadInt32(count1)) + assert.Nil(t, getVal(oncePrefix+jobKey1)) + + assert.Empty(t, s.activeJobs.jobs[jobKey1]) + assert.Empty(t, getVal(oncePrefix+jobKey1)) + assert.Equal(t, int32(0), atomic.LoadInt32(count1)) + + s.pluginAPI.(*mockPluginAPI).setFailingWithPrefix("") + }) + + t.Run("simulate starting the plugin with 3 pending jobs in the db", func(t *testing.T) { + resetScheduler() + + jobKeys := make(map[string]*int32) + for i := 0; i < 3; i++ { + jobKeys[makeKey()] = new(int32) + } + + callback := func(key string, _ any) { + count, ok := jobKeys[key] + if ok { + atomic.AddInt32(count, 1) + } + } + err := s.SetCallback(callback) + require.NoError(t, err) + err = s.Start() + require.NoError(t, err) + + for k := range jobKeys { + job, err3 := newJobOnce(s.pluginAPI, k, time.Now().Add(100*time.Millisecond), s.storedCallback, s.activeJobs, nil) + require.NoError(t, err3) + err3 = job.saveMetadata() + require.NoError(t, err3) + assert.NotEmpty(t, getVal(oncePrefix+k)) + } + + // double checking they're in the db: + jobs, err := s.ListScheduledJobs() + require.NoError(t, err) + require.Len(t, jobs, 3) + + // simulate starting the plugin + require.NoError(t, err) + err = s.scheduleNewJobsFromDB() + require.NoError(t, err) + + time.Sleep(120*time.Millisecond + scheduleOnceJitter) + + for k, v := range jobKeys { + assert.Empty(t, getVal(oncePrefix+k)) + assert.Empty(t, s.activeJobs.jobs[k]) + assert.Equal(t, int32(1), *v) + } + jobs, err = s.ListScheduledJobs() + require.NoError(t, err) + require.Empty(t, jobs) + }) + + t.Run("starting a job and polling before it's finished results in only one job running", func(t *testing.T) { + resetScheduler() + + jobKey := makeKey() + count := new(int32) + + callback := func(key string, _ any) { + if key == jobKey { + atomic.AddInt32(count, 1) + } + } + + err := s.SetCallback(callback) + require.NoError(t, err) + err = s.Start() + require.NoError(t, err) + + jobs, err := s.ListScheduledJobs() + require.NoError(t, err) + require.Empty(t, jobs) + + job, err := s.ScheduleOnce(jobKey, time.Now().Add(100*time.Millisecond), nil) + require.NoError(t, err) + require.NotNil(t, job) + assert.NotEmpty(t, getVal(oncePrefix+jobKey)) + s.activeJobs.mu.Lock() + assert.NotEmpty(t, s.activeJobs.jobs[jobKey]) + assert.Len(t, s.activeJobs.jobs, 1) + s.activeJobs.mu.Unlock() + + // simulate what the polling function will do for a long running job: + err = s.scheduleNewJobsFromDB() + require.NoError(t, err) + err = s.scheduleNewJobsFromDB() + require.NoError(t, err) + err = s.scheduleNewJobsFromDB() + require.NoError(t, err) + assert.NotEmpty(t, getVal(oncePrefix+jobKey)) + s.activeJobs.mu.Lock() + assert.NotEmpty(t, s.activeJobs.jobs[jobKey]) + assert.Len(t, s.activeJobs.jobs, 1) + s.activeJobs.mu.Unlock() + + // now wait for it to complete + time.Sleep(120*time.Millisecond + scheduleOnceJitter) + assert.Equal(t, int32(1), atomic.LoadInt32(count)) + assert.Empty(t, getVal(oncePrefix+jobKey)) + s.activeJobs.mu.Lock() + assert.Empty(t, s.activeJobs.jobs) + s.activeJobs.mu.Unlock() + }) + + t.Run("starting the same job again while it's still active will fail", func(t *testing.T) { + resetScheduler() + + jobKey := makeKey() + count := new(int32) + + callback := func(key string, _ any) { + if key == jobKey { + atomic.AddInt32(count, 1) + } + } + + err := s.SetCallback(callback) + require.NoError(t, err) + err = s.Start() + require.NoError(t, err) + + jobs, err := s.ListScheduledJobs() + require.NoError(t, err) + require.Empty(t, jobs) + + job, err := s.ScheduleOnce(jobKey, time.Now().Add(100*time.Millisecond), nil) + require.NoError(t, err) + require.NotNil(t, job) + assert.NotEmpty(t, getVal(oncePrefix+jobKey)) + assert.NotEmpty(t, s.activeJobs.jobs[jobKey]) + assert.Len(t, s.activeJobs.jobs, 1) + + // a plugin tries to start the same jobKey again: + job, err = s.ScheduleOnce(jobKey, time.Now().Add(10000*time.Millisecond), nil) + require.Error(t, err) + require.Nil(t, job) + + // now wait for first job to complete + time.Sleep(120*time.Millisecond + scheduleOnceJitter) + assert.Equal(t, int32(1), atomic.LoadInt32(count)) + assert.Empty(t, getVal(oncePrefix+jobKey)) + assert.Empty(t, s.activeJobs.jobs) + }) + + t.Run("simulate HA: canceling and setting a job with a different time--old one shouldn't fire", func(t *testing.T) { + resetScheduler() + + key := makeKey() + jobKeys := make(map[string]*int32) + jobKeys[key] = new(int32) + + // control is like the "control group" in an experiment. It will be overwritten, + // but with the same runAt. It should fire. + control := makeKey() + jobKeys[control] = new(int32) + + callback := func(key string, _ any) { + count, ok := jobKeys[key] + if ok { + atomic.AddInt32(count, 1) + } + } + err := s.SetCallback(callback) + require.NoError(t, err) + err = s.Start() + require.NoError(t, err) + + originalRunAt := time.Now().Add(100 * time.Millisecond) + newRunAt := time.Now().Add(101 * time.Millisecond) + + // store original + job, err := newJobOnce(s.pluginAPI, key, originalRunAt, s.storedCallback, s.activeJobs, nil) + require.NoError(t, err) + err = job.saveMetadata() + require.NoError(t, err) + assert.NotEmpty(t, getVal(oncePrefix+key)) + + // store oringal control + job2, err := newJobOnce(s.pluginAPI, control, originalRunAt, s.storedCallback, s.activeJobs, nil) + require.NoError(t, err) + err = job2.saveMetadata() + require.NoError(t, err) + assert.NotEmpty(t, getVal(oncePrefix+control)) + + // double checking originals are in the db: + jobs, err := s.ListScheduledJobs() + require.NoError(t, err) + require.Len(t, jobs, 2) + require.True(t, originalRunAt.Equal(jobs[0].RunAt)) + require.True(t, originalRunAt.Equal(jobs[1].RunAt)) + + // simulate starting the plugin + require.NoError(t, err) + err = s.scheduleNewJobsFromDB() + require.NoError(t, err) + + // Now "cancel" the original and make a new job with the same key but a different time. + // However, because we have only one list of synced jobs, we can't make two jobs with the + // same key. So we'll simulate this by changing the job metadata in the db. When the original + // job fires, it should see that the runAt is different, and it will think it has been canceled. + err = setMetadata(key, JobOnceMetadata{ + Key: key, + RunAt: newRunAt, + }) + require.NoError(t, err) + + // overwrite the control with the same runAt. It should fire. + err = setMetadata(control, JobOnceMetadata{ + Key: control, + RunAt: originalRunAt, + }) + require.NoError(t, err) + + time.Sleep(120*time.Millisecond + scheduleOnceJitter) + + // original job didn't fire the callback: + assert.Empty(t, getVal(oncePrefix+key)) + assert.Empty(t, s.activeJobs.jobs[key]) + assert.Equal(t, int32(0), *jobKeys[key]) + + // control job did fire the callback: + assert.Empty(t, getVal(oncePrefix+control)) + assert.Empty(t, s.activeJobs.jobs[control]) + assert.Equal(t, int32(1), *jobKeys[control]) + + jobs, err = s.ListScheduledJobs() + require.NoError(t, err) + require.Empty(t, jobs) + }) +} + +func TestScheduleOnceProps(t *testing.T) { + t.Run("confirm props are returned", func(t *testing.T) { + s := GetJobOnceScheduler(newMockPluginAPI(t)) + + jobKey := model.NewId() + jobProps := struct { + Foo string + }{ + Foo: "some foo", + } + + var mut sync.Mutex + var called bool + callback := func(key string, props any) { + require.Equal(t, jobKey, key) + require.Equal(t, jobProps, props) + mut.Lock() + defer mut.Unlock() + called = true + } + + err := s.SetCallback(callback) + require.NoError(t, err) + if !s.started { + err = s.Start() + require.NoError(t, err) + } + + _, err = s.ScheduleOnce(jobKey, time.Now().Add(100*time.Millisecond), jobProps) + require.NoError(t, err) + + // Check if callback was called + require.Eventually(t, func() bool { mut.Lock(); defer mut.Unlock(); return called }, time.Second, 50*time.Millisecond) + }) + + t.Run("props to large", func(t *testing.T) { + s := GetJobOnceScheduler(newMockPluginAPI(t)) + + props := make([]byte, propsLimit) + for i := 0; i < propsLimit; i++ { + props[i] = 'a' + } + + _, err := s.ScheduleOnce(model.NewId(), time.Now().Add(100*time.Millisecond), props) + require.Error(t, err) + }) +} diff --git a/server/public/pluginapi/cluster/job_test.go b/server/public/pluginapi/cluster/job_test.go new file mode 100644 index 00000000000..49019ae800b --- /dev/null +++ b/server/public/pluginapi/cluster/job_test.go @@ -0,0 +1,412 @@ +package cluster + +import ( + "sync" + "sync/atomic" + "testing" + "time" + + "github.com/mattermost/mattermost/server/public/model" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestMakeWaitForInterval(t *testing.T) { + t.Run("panics on invalid interval", func(t *testing.T) { + assert.Panics(t, func() { + MakeWaitForInterval(0) + }) + }) + + const neverRun = -1 * time.Second + + testCases := []struct { + Description string + Interval time.Duration + LastFinished time.Duration + Expected time.Duration + }{ + { + "never run, 5 minutes", + 5 * time.Minute, + neverRun, + 0, + }, + { + "run 1 minute ago, 5 minutes", + 5 * time.Minute, + -1 * time.Minute, + 4 * time.Minute, + }, + { + "run 2 minutes ago, 5 minutes", + 5 * time.Minute, + -2 * time.Minute, + 3 * time.Minute, + }, + { + "run 4 minutes 30 seconds ago, 5 minutes", + 5 * time.Minute, + -4*time.Minute - 30*time.Second, + 30 * time.Second, + }, + { + "run 4 minutes 59 seconds ago, 5 minutes", + 5 * time.Minute, + -4*time.Minute - 59*time.Second, + 1 * time.Second, + }, + { + "never run, 1 hour", + 1 * time.Hour, + neverRun, + 0, + }, + { + "run 1 minute ago, 1 hour", + 1 * time.Hour, + -1 * time.Minute, + 59 * time.Minute, + }, + { + "run 20 minutes ago, 1 hour", + 1 * time.Hour, + -20 * time.Minute, + 40 * time.Minute, + }, + { + "run 55 minutes 30 seconds ago, 1 hour", + 1 * time.Hour, + -55*time.Minute - 30*time.Second, + 4*time.Minute + 30*time.Second, + }, + { + "run 59 minutes 59 seconds ago, 1 hour", + 1 * time.Hour, + -59*time.Minute - 59*time.Second, + 1 * time.Second, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.Description, func(t *testing.T) { + now := time.Now() + + var lastFinished time.Time + if testCase.LastFinished != neverRun { + lastFinished = now.Add(testCase.LastFinished) + } + + actual := MakeWaitForInterval(testCase.Interval)(now, JobMetadata{ + LastFinished: lastFinished, + }) + assert.Equal(t, testCase.Expected, actual) + }) + } +} + +func TestMakeWaitForRoundedInterval(t *testing.T) { + t.Run("panics on invalid interval", func(t *testing.T) { + assert.Panics(t, func() { + MakeWaitForRoundedInterval(0) + }) + }) + + const neverRun = -1 * time.Second + topOfTheHour := time.Now().Truncate(1 * time.Hour) + topOfTheDay := time.Now().Truncate(24 * time.Hour) + + testCases := []struct { + Description string + Interval time.Duration + Now time.Time + LastFinished time.Duration + Expected time.Duration + }{ + { + "5 minutes, top of the hour, never run", + 5 * time.Minute, + topOfTheHour, + neverRun, + 0, + }, + { + "5 minutes, top of the hour less 1 minute, never run", + 5 * time.Minute, + topOfTheHour.Add(-1 * time.Minute), + neverRun, + 0, + }, + { + "5 minutes, top of the hour less 1 minute, run 1 minute ago", + 5 * time.Minute, + topOfTheHour.Add(-1 * time.Minute), + -1 * time.Minute, + 1 * time.Minute, + }, + { + "5 minutes, top of the hour plus 1 minute, run 2 minutes ago", + 5 * time.Minute, + topOfTheHour.Add(1 * time.Minute), + -2 * time.Minute, + 0, + }, + { + "5 minutes, top of the hour plus 1 minute, run 30 seconds ago", + 5 * time.Minute, + topOfTheHour.Add(1 * time.Minute), + -30 * time.Second, + 4 * time.Minute, + }, + { + "5 minutes, top of the hour plus 7 minutes, run 30 seconds ago", + 5 * time.Minute, + topOfTheHour.Add(7 * time.Minute), + -30 * time.Second, + 3 * time.Minute, + }, + { + "30 minutes, top of the hour, never run", + 30 * time.Minute, + topOfTheHour, + neverRun, + 0, + }, + { + "30 minutes, top of the hour less 1 minute, never run", + 30 * time.Minute, + topOfTheHour.Add(-1 * time.Minute), + neverRun, + 0, + }, + { + "30 minutes, top of the hour less 1 minute, run 1 minute ago", + 30 * time.Minute, + topOfTheHour.Add(-1 * time.Minute), + -1 * time.Minute, + 1 * time.Minute, + }, + { + "30 minutes, top of the hour plus 1 minute, run 2 minutes ago", + 30 * time.Minute, + topOfTheHour.Add(1 * time.Minute), + -2 * time.Minute, + 0, + }, + { + "30 minutes, top of the hour plus 1 minute, run 30 seconds ago", + 30 * time.Minute, + topOfTheHour.Add(1 * time.Minute), + -30 * time.Second, + 29 * time.Minute, + }, + { + "30 minutes, top of the hour plus 7 minutes, run 30 seconds ago", + 30 * time.Minute, + topOfTheHour.Add(7 * time.Minute), + -30 * time.Second, + 23 * time.Minute, + }, + { + "24 hours, top of the day, never run", + 24 * time.Hour, + topOfTheDay, + neverRun, + 0, + }, + { + "24 hours, top of the day less 1 minute, never run", + 24 * time.Hour, + topOfTheDay.Add(-1 * time.Minute), + neverRun, + 0, + }, + { + "24 hours, top of the day less 1 minute, run 1 minute ago", + 24 * time.Hour, + topOfTheDay.Add(-1 * time.Minute), + -1 * time.Minute, + 1 * time.Minute, + }, + { + "24 hours, top of the day plus 1 minute, run 2 minutes ago", + 24 * time.Hour, + topOfTheDay.Add(1 * time.Minute), + -2 * time.Minute, + 0, + }, + { + "24 hours, top of the day plus 1 minute, run 30 seconds ago", + 24 * time.Hour, + topOfTheDay.Add(1 * time.Minute), + -30 * time.Second, + 23*time.Hour + 59*time.Minute, + }, + { + "24 hours, top of the day plus 7 minutes, run 30 seconds ago", + 24 * time.Hour, + topOfTheDay.Add(7 * time.Minute), + -30 * time.Second, + 23*time.Hour + 53*time.Minute, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.Description, func(t *testing.T) { + var lastFinished time.Time + if testCase.LastFinished != neverRun { + lastFinished = testCase.Now.Add(testCase.LastFinished) + } + + actual := MakeWaitForRoundedInterval(testCase.Interval)(testCase.Now, JobMetadata{ + LastFinished: lastFinished, + }) + assert.Equal(t, testCase.Expected, actual) + }) + } +} + +func TestSchedule(t *testing.T) { + t.Parallel() + + makeKey := model.NewId + + t.Run("single-threaded", func(t *testing.T) { + t.Parallel() + + mockPluginAPI := newMockPluginAPI(t) + + count := new(int32) + callback := func() { + atomic.AddInt32(count, 1) + } + + job, err := Schedule(mockPluginAPI, makeKey(), MakeWaitForInterval(100*time.Millisecond), callback) + require.NoError(t, err) + require.NotNil(t, job) + + time.Sleep(1 * time.Second) + + err = job.Close() + require.NoError(t, err) + + time.Sleep(1 * time.Second) + + // Shouldn't have hit 20 in this time frame + assert.Less(t, *count, int32(20)) + + // Should have hit at least 5 in this time frame + assert.Greater(t, *count, int32(5)) + }) + + t.Run("multi-threaded, single job", func(t *testing.T) { + t.Parallel() + + mockPluginAPI := newMockPluginAPI(t) + + count := new(int32) + callback := func() { + atomic.AddInt32(count, 1) + } + + var jobs []*Job + + key := makeKey() + + for i := 0; i < 3; i++ { + job, err := Schedule(mockPluginAPI, key, MakeWaitForInterval(100*time.Millisecond), callback) + require.NoError(t, err) + require.NotNil(t, job) + + jobs = append(jobs, job) + } + + time.Sleep(1 * time.Second) + + var wg sync.WaitGroup + for i := 0; i < 3; i++ { + job := jobs[i] + wg.Add(1) + go func() { + defer wg.Done() + err := job.Close() + require.NoError(t, err) + }() + } + wg.Wait() + + time.Sleep(1 * time.Second) + + // Shouldn't have hit 20 in this time frame + assert.Less(t, *count, int32(20)) + + // Should have hit at least 5 in this time frame + assert.Greater(t, *count, int32(5)) + }) + + t.Run("multi-threaded, multiple jobs", func(t *testing.T) { + t.Parallel() + + mockPluginAPI := newMockPluginAPI(t) + + countA := new(int32) + callbackA := func() { + atomic.AddInt32(countA, 1) + } + + countB := new(int32) + callbackB := func() { + atomic.AddInt32(countB, 1) + } + + keyA := makeKey() + keyB := makeKey() + + var jobs []*Job + for i := 0; i < 3; i++ { + var key string + var callback func() + if i <= 1 { + key = keyA + callback = callbackA + } else { + key = keyB + callback = callbackB + } + + job, err := Schedule(mockPluginAPI, key, MakeWaitForInterval(100*time.Millisecond), callback) + require.NoError(t, err) + require.NotNil(t, job) + + jobs = append(jobs, job) + } + + time.Sleep(1 * time.Second) + + var wg sync.WaitGroup + for i := 0; i < 3; i++ { + job := jobs[i] + wg.Add(1) + go func() { + defer wg.Done() + err := job.Close() + require.NoError(t, err) + }() + } + wg.Wait() + + time.Sleep(1 * time.Second) + + // Shouldn't have hit 20 in this time frame + assert.Less(t, *countA, int32(20)) + + // Should have hit at least 5 in this time frame + assert.Greater(t, *countA, int32(5)) + + // Shouldn't have hit 20 in this time frame + assert.Less(t, *countB, int32(20)) + + // Should have hit at least 5 in this time frame + assert.Greater(t, *countB, int32(5)) + }) +} diff --git a/server/public/pluginapi/cluster/mock_plugin_api_test.go b/server/public/pluginapi/cluster/mock_plugin_api_test.go new file mode 100644 index 00000000000..5a5487a4ae4 --- /dev/null +++ b/server/public/pluginapi/cluster/mock_plugin_api_test.go @@ -0,0 +1,150 @@ +package cluster + +import ( + "bytes" + "sort" + "strings" + "sync" + "testing" + + "github.com/mattermost/mattermost/server/public/model" +) + +type mockPluginAPI struct { + t *testing.T + + lock sync.Mutex + keyValues map[string][]byte + failing bool + failingWithPrefix string +} + +func newMockPluginAPI(t *testing.T) *mockPluginAPI { + return &mockPluginAPI{ + t: t, + keyValues: make(map[string][]byte), + } +} + +func (pluginAPI *mockPluginAPI) setFailing(failing bool) { + pluginAPI.lock.Lock() + defer pluginAPI.lock.Unlock() + + pluginAPI.failing = failing +} + +func (pluginAPI *mockPluginAPI) setFailingWithPrefix(prefix string) { + pluginAPI.lock.Lock() + defer pluginAPI.lock.Unlock() + + pluginAPI.failingWithPrefix = prefix +} + +func (pluginAPI *mockPluginAPI) clear() { + pluginAPI.lock.Lock() + defer pluginAPI.lock.Unlock() + + for k := range pluginAPI.keyValues { + delete(pluginAPI.keyValues, k) + } +} + +func (pluginAPI *mockPluginAPI) KVGet(key string) ([]byte, *model.AppError) { + pluginAPI.lock.Lock() + defer pluginAPI.lock.Unlock() + + if pluginAPI.failing { + return nil, &model.AppError{Message: "fake error"} + } + + if pluginAPI.failingWithPrefix != "" && strings.HasPrefix(key, pluginAPI.failingWithPrefix) { + return nil, &model.AppError{Message: "fake error for prefix " + pluginAPI.failingWithPrefix} + } + + return pluginAPI.keyValues[key], nil +} + +func (pluginAPI *mockPluginAPI) KVDelete(key string) *model.AppError { + pluginAPI.lock.Lock() + defer pluginAPI.lock.Unlock() + + if pluginAPI.failing { + return &model.AppError{Message: "fake error"} + } + + if pluginAPI.failingWithPrefix != "" && strings.HasPrefix(key, pluginAPI.failingWithPrefix) { + return &model.AppError{Message: "fake error for prefix " + pluginAPI.failingWithPrefix} + } + + delete(pluginAPI.keyValues, key) + + return nil +} + +func (pluginAPI *mockPluginAPI) KVList(page, count int) ([]string, *model.AppError) { + pluginAPI.lock.Lock() + defer pluginAPI.lock.Unlock() + + if pluginAPI.failing { + return nil, &model.AppError{Message: "fake error"} + } + + keys := make([]string, 0, len(pluginAPI.keyValues)) + for k := range pluginAPI.keyValues { + keys = append(keys, k) + } + + // have to sort, because we're paging below + sort.Strings(keys) + + start := min(page*count, len(keys)) + end := min((page+1)*count, len(keys)) + return keys[start:end], nil +} + +func min(a, b int) int { + if a < b { + return a + } + return b +} + +func (pluginAPI *mockPluginAPI) KVSetWithOptions(key string, value []byte, options model.PluginKVSetOptions) (bool, *model.AppError) { + pluginAPI.lock.Lock() + defer pluginAPI.lock.Unlock() + + if pluginAPI.failing { + return false, &model.AppError{Message: "fake error"} + } + + if pluginAPI.failingWithPrefix != "" && strings.HasPrefix(key, pluginAPI.failingWithPrefix) { + return false, &model.AppError{Message: "fake error for prefix " + pluginAPI.failingWithPrefix} + } + + if options.Atomic { + if actualValue := pluginAPI.keyValues[key]; !bytes.Equal(actualValue, options.OldValue) { + return false, nil + } + } + + if value == nil { + delete(pluginAPI.keyValues, key) + } else { + pluginAPI.keyValues[key] = value + } + + return true, nil +} + +func (pluginAPI *mockPluginAPI) LogError(msg string, keyValuePairs ...interface{}) { + if pluginAPI.t == nil { + return + } + + pluginAPI.t.Helper() + + params := []interface{}{msg} + params = append(params, keyValuePairs...) + + pluginAPI.t.Log(params...) +} diff --git a/server/public/pluginapi/cluster/mutex.go b/server/public/pluginapi/cluster/mutex.go new file mode 100644 index 00000000000..0169d3b398b --- /dev/null +++ b/server/public/pluginapi/cluster/mutex.go @@ -0,0 +1,185 @@ +package cluster + +import ( + "context" + "sync" + "time" + + "github.com/mattermost/mattermost/server/public/model" + "github.com/pkg/errors" +) + +const ( + // mutexPrefix is used to namespace key values created for a mutex from other key values + // created by a plugin. + mutexPrefix = "mutex_" +) + +const ( + // ttl is the interval after which a locked mutex will expire unless refreshed + ttl = time.Second * 15 + + // refreshInterval is the interval on which the mutex will be refreshed when locked + refreshInterval = ttl / 2 +) + +// MutexPluginAPI is the plugin API interface required to manage mutexes. +type MutexPluginAPI interface { + KVSetWithOptions(key string, value []byte, options model.PluginKVSetOptions) (bool, *model.AppError) + LogError(msg string, keyValuePairs ...interface{}) +} + +// Mutex is similar to sync.Mutex, except usable by multiple plugin instances across a cluster. +// +// Internally, a mutex relies on an atomic key-value set operation as exposed by the Mattermost +// plugin API. +// +// Mutexes with different names are unrelated. Mutexes with the same name from different plugins +// are unrelated. Pick a unique name for each mutex your plugin requires. +// +// A Mutex must not be copied after first use. +type Mutex struct { + pluginAPI MutexPluginAPI + key string + + // lock guards the variables used to manage the refresh task, and is not itself related to + // the cluster-wide lock. + lock sync.Mutex + stopRefresh chan bool + refreshDone chan bool +} + +// NewMutex creates a mutex with the given key name. +// +// Panics if key is empty. +func NewMutex(pluginAPI MutexPluginAPI, key string) (*Mutex, error) { + key, err := makeLockKey(key) + if err != nil { + return nil, err + } + + return &Mutex{ + pluginAPI: pluginAPI, + key: key, + }, nil +} + +// makeLockKey returns the prefixed key used to namespace mutex keys. +func makeLockKey(key string) (string, error) { + if key == "" { + return "", errors.New("must specify valid mutex key") + } + + return mutexPrefix + key, nil +} + +// lock makes a single attempt to atomically lock the mutex, returning true only if successful. +func (m *Mutex) tryLock() (bool, error) { + ok, err := m.pluginAPI.KVSetWithOptions(m.key, []byte{1}, model.PluginKVSetOptions{ + Atomic: true, + OldValue: nil, // No existing key value. + ExpireInSeconds: int64(ttl / time.Second), + }) + if err != nil { + return false, errors.Wrap(err, "failed to set mutex kv") + } + + return ok, nil +} + +// refreshLock rewrites the lock key value with a new expiry, returning true only if successful. +func (m *Mutex) refreshLock() error { + ok, err := m.pluginAPI.KVSetWithOptions(m.key, []byte{1}, model.PluginKVSetOptions{ + Atomic: true, + OldValue: []byte{1}, + ExpireInSeconds: int64(ttl / time.Second), + }) + if err != nil { + return errors.Wrap(err, "failed to refresh mutex kv") + } else if !ok { + return errors.New("unexpectedly failed to refresh mutex kv") + } + + return nil +} + +// Lock locks m. If the mutex is already locked by any plugin instance, including the current one, +// the calling goroutine blocks until the mutex can be locked. +func (m *Mutex) Lock() { + _ = m.LockWithContext(context.Background()) +} + +// LockWithContext locks m unless the context is canceled. If the mutex is already locked by any plugin +// instance, including the current one, the calling goroutine blocks until the mutex can be locked, +// or the context is canceled. +// +// The mutex is locked only if a nil error is returned. +func (m *Mutex) LockWithContext(ctx context.Context) error { + var waitInterval time.Duration + + for { + select { + case <-ctx.Done(): + return ctx.Err() + case <-time.After(waitInterval): + } + + locked, err := m.tryLock() + if err != nil { + m.pluginAPI.LogError("failed to lock mutex", "err", err, "lock_key", m.key) + waitInterval = nextWaitInterval(waitInterval, err) + continue + } else if !locked { + waitInterval = nextWaitInterval(waitInterval, err) + continue + } + + stop := make(chan bool) + done := make(chan bool) + go func() { + defer close(done) + t := time.NewTicker(refreshInterval) + for { + select { + case <-t.C: + err := m.refreshLock() + if err != nil { + m.pluginAPI.LogError("failed to refresh mutex", "err", err, "lock_key", m.key) + return + } + case <-stop: + return + } + } + }() + + m.lock.Lock() + m.stopRefresh = stop + m.refreshDone = done + m.lock.Unlock() + + return nil + } +} + +// Unlock unlocks m. It is a run-time error if m is not locked on entry to Unlock. +// +// Just like sync.Mutex, a locked Lock is not associated with a particular goroutine or plugin +// instance. It is allowed for one goroutine or plugin instance to lock a Lock and then arrange +// for another goroutine or plugin instance to unlock it. In practice, ownership of the lock should +// remain within a single plugin instance. +func (m *Mutex) Unlock() { + m.lock.Lock() + if m.stopRefresh == nil { + m.lock.Unlock() + panic("mutex has not been acquired") + } + + close(m.stopRefresh) + m.stopRefresh = nil + <-m.refreshDone + m.lock.Unlock() + + // If an error occurs deleting, the mutex kv will still expire, allowing later retry. + _, _ = m.pluginAPI.KVSetWithOptions(m.key, nil, model.PluginKVSetOptions{}) +} diff --git a/server/public/pluginapi/cluster/mutex_example_test.go b/server/public/pluginapi/cluster/mutex_example_test.go new file mode 100644 index 00000000000..8db36c50231 --- /dev/null +++ b/server/public/pluginapi/cluster/mutex_example_test.go @@ -0,0 +1,20 @@ +package cluster_test + +import ( + "github.com/mattermost/mattermost/server/public/plugin" + "github.com/mattermost/mattermost/server/public/pluginapi/cluster" +) + +//nolint:staticcheck +func ExampleMutex() { + // Use p.API from your plugin instead. + pluginAPI := plugin.API(nil) + + m, err := cluster.NewMutex(pluginAPI, "key") + if err != nil { + panic(err) + } + m.Lock() + // critical section + m.Unlock() +} diff --git a/server/public/pluginapi/cluster/mutex_test.go b/server/public/pluginapi/cluster/mutex_test.go new file mode 100644 index 00000000000..d415ee736bb --- /dev/null +++ b/server/public/pluginapi/cluster/mutex_test.go @@ -0,0 +1,276 @@ +package cluster + +import ( + "context" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/mattermost/mattermost/server/public/model" +) + +func mustNewMutex(pluginAPI MutexPluginAPI, key string) *Mutex { + m, err := NewMutex(pluginAPI, key) + if err != nil { + panic(err) + } + + return m +} + +func TestMakeLockKey(t *testing.T) { + t.Run("fails when empty", func(t *testing.T) { + key, err := makeLockKey("") + assert.Error(t, err) + assert.Empty(t, key) + }) + + t.Run("not-empty", func(t *testing.T) { + testCases := map[string]string{ + "key": mutexPrefix + "key", + "other": mutexPrefix + "other", + } + + for key, expected := range testCases { + actual, err := makeLockKey(key) + require.NoError(t, err) + assert.Equal(t, expected, actual) + } + }) +} + +func lock(t *testing.T, m *Mutex) { + t.Helper() + + done := make(chan bool) + go func() { + t.Helper() + + defer close(done) + m.Lock() + }() + + select { + case <-time.After(1 * time.Second): + require.Fail(t, "failed to lock mutex within 1 second") + case <-done: + } +} + +func unlock(t *testing.T, m *Mutex, panics bool) { + t.Helper() + + done := make(chan bool) + go func() { + t.Helper() + + defer close(done) + if panics { + assert.Panics(t, m.Unlock) + } else { + assert.NotPanics(t, m.Unlock) + } + }() + + select { + case <-time.After(1 * time.Second): + require.Fail(t, "failed to unlock mutex within 1 second") + case <-done: + } +} + +func TestMutex(t *testing.T) { + t.Parallel() + + makeKey := model.NewId + + t.Run("successful lock/unlock cycle", func(t *testing.T) { + t.Parallel() + + mockPluginAPI := newMockPluginAPI(t) + + m := mustNewMutex(mockPluginAPI, makeKey()) + lock(t, m) + unlock(t, m, false) + lock(t, m) + unlock(t, m, false) + }) + + t.Run("unlock when not locked", func(t *testing.T) { + t.Parallel() + + mockPluginAPI := newMockPluginAPI(t) + + m := mustNewMutex(mockPluginAPI, makeKey()) + unlock(t, m, true) + }) + + t.Run("blocking lock", func(t *testing.T) { + t.Parallel() + + mockPluginAPI := newMockPluginAPI(t) + + m := mustNewMutex(mockPluginAPI, makeKey()) + lock(t, m) + + done := make(chan bool) + go func() { + defer close(done) + m.Lock() + }() + + select { + case <-time.After(1 * time.Second): + case <-done: + require.Fail(t, "second goroutine should not have locked") + } + + unlock(t, m, false) + + select { + case <-time.After(pollWaitInterval * 2): + require.Fail(t, "second goroutine should have locked") + case <-done: + } + }) + + t.Run("failed lock", func(t *testing.T) { + t.Parallel() + + mockPluginAPI := newMockPluginAPI(t) + + m := mustNewMutex(mockPluginAPI, makeKey()) + + mockPluginAPI.setFailing(true) + + done := make(chan bool) + go func() { + defer close(done) + m.Lock() + }() + + select { + case <-time.After(5 * time.Second): + case <-done: + require.Fail(t, "goroutine should not have locked") + } + + mockPluginAPI.setFailing(false) + + select { + case <-time.After(15 * time.Second): + require.Fail(t, "goroutine should have locked") + case <-done: + } + }) + + t.Run("failed unlock", func(t *testing.T) { + t.Parallel() + + mockPluginAPI := newMockPluginAPI(t) + + key := makeKey() + m := mustNewMutex(mockPluginAPI, key) + lock(t, m) + + mockPluginAPI.setFailing(true) + + unlock(t, m, false) + + // Simulate expiry + mockPluginAPI.clear() + mockPluginAPI.setFailing(false) + + lock(t, m) + }) + + t.Run("discrete keys", func(t *testing.T) { + t.Parallel() + + mockPluginAPI := newMockPluginAPI(t) + + m1 := mustNewMutex(mockPluginAPI, makeKey()) + lock(t, m1) + + m2 := mustNewMutex(mockPluginAPI, makeKey()) + lock(t, m2) + + m3 := mustNewMutex(mockPluginAPI, makeKey()) + lock(t, m3) + + unlock(t, m1, false) + unlock(t, m3, false) + + lock(t, m1) + + unlock(t, m2, false) + unlock(t, m1, false) + }) + + t.Run("with uncancelled context", func(t *testing.T) { + t.Parallel() + + mockPluginAPI := newMockPluginAPI(t) + + key := makeKey() + m := mustNewMutex(mockPluginAPI, key) + + m.Lock() + + ctx := context.Background() + done := make(chan bool) + go func() { + defer close(done) + err := m.LockWithContext(ctx) + require.Nil(t, err) + }() + + select { + case <-time.After(ttl + pollWaitInterval*2): + case <-done: + require.Fail(t, "goroutine should not have locked") + } + + m.Unlock() + + select { + case <-time.After(pollWaitInterval * 2): + require.Fail(t, "goroutine should have locked after unlock") + case <-done: + } + }) + + t.Run("with canceled context", func(t *testing.T) { + t.Parallel() + + mockPluginAPI := newMockPluginAPI(t) + + m := mustNewMutex(mockPluginAPI, makeKey()) + + m.Lock() + + ctx, cancel := context.WithCancel(context.Background()) + done := make(chan bool) + go func() { + defer close(done) + err := m.LockWithContext(ctx) + require.NotNil(t, err) + }() + + select { + case <-time.After(ttl + pollWaitInterval*2): + case <-done: + require.Fail(t, "goroutine should not have locked") + } + + cancel() + + select { + case <-time.After(pollWaitInterval * 2): + require.Fail(t, "goroutine should have aborted after cancellation") + case <-done: + } + }) +} diff --git a/server/public/pluginapi/cluster/wait.go b/server/public/pluginapi/cluster/wait.go new file mode 100644 index 00000000000..bf62b4ac8d0 --- /dev/null +++ b/server/public/pluginapi/cluster/wait.go @@ -0,0 +1,43 @@ +package cluster + +import ( + "math/rand" + "time" +) + +const ( + // minWaitInterval is the minimum amount of time to wait between locking attempts + minWaitInterval = 1 * time.Second + + // maxWaitInterval is the maximum amount of time to wait between locking attempts + maxWaitInterval = 5 * time.Minute + + // pollWaitInterval is the usual time to wait between unsuccessful locking attempts + pollWaitInterval = 1 * time.Second + + // jitterWaitInterval is the amount of jitter to add when waiting to avoid thundering herds + jitterWaitInterval = minWaitInterval / 2 +) + +// nextWaitInterval determines how long to wait until the next lock retry. +func nextWaitInterval(lastWaitInterval time.Duration, err error) time.Duration { + nextWaitInterval := lastWaitInterval + + if nextWaitInterval <= 0 { + nextWaitInterval = minWaitInterval + } + + if err != nil { + nextWaitInterval *= 2 + if nextWaitInterval > maxWaitInterval { + nextWaitInterval = maxWaitInterval + } + } else { + nextWaitInterval = pollWaitInterval + } + + // Add some jitter to avoid unnecessary collision between competing plugin instances. + nextWaitInterval += time.Duration(rand.Int63n(int64(jitterWaitInterval)) - int64(jitterWaitInterval)/2) + + return nextWaitInterval +} diff --git a/server/public/pluginapi/cluster/wait_test.go b/server/public/pluginapi/cluster/wait_test.go new file mode 100644 index 00000000000..fb6f35ae925 --- /dev/null +++ b/server/public/pluginapi/cluster/wait_test.go @@ -0,0 +1,156 @@ +package cluster + +import ( + "testing" + "time" + + "github.com/pkg/errors" + "github.com/stretchr/testify/assert" +) + +func TestNextWaitInterval(t *testing.T) { + testCases := []struct { + Description string + lastWaitInterval time.Duration + err error + expectedRange [2]time.Duration + }{ + { + "0, no error", + 0, + nil, + [2]time.Duration{ + 1*time.Second - jitterWaitInterval/2, + 1*time.Second + jitterWaitInterval/2, + }, + }, + { + "0, error", + 0, + errors.New("test"), + [2]time.Duration{ + 2*time.Second - jitterWaitInterval/2, + 2*time.Second + jitterWaitInterval/2, + }, + }, + { + "negative, no error", + -100 * time.Second, + nil, + [2]time.Duration{ + 1*time.Second - jitterWaitInterval/2, + 1*time.Second + jitterWaitInterval/2, + }, + }, + { + "negative, error", + -100 * time.Second, + errors.New("test"), + [2]time.Duration{ + 2*time.Second - jitterWaitInterval/2, + 2*time.Second + jitterWaitInterval/2, + }, + }, + { + "1 second, no error", + 1 * time.Second, + nil, + [2]time.Duration{ + 1*time.Second - jitterWaitInterval/2, + 1*time.Second + jitterWaitInterval/2, + }, + }, + { + "1 second, error", + 1 * time.Second, + errors.New("test"), + [2]time.Duration{ + 2*time.Second - jitterWaitInterval/2, + 2*time.Second + jitterWaitInterval/2, + }, + }, + { + "10 seconds, no error", + 10 * time.Second, + nil, + [2]time.Duration{ + 1*time.Second - jitterWaitInterval/2, + 1*time.Second + jitterWaitInterval/2, + }, + }, + { + "10 second, error", + 10 * time.Second, + errors.New("test"), + [2]time.Duration{ + 20*time.Second - jitterWaitInterval/2, + 20*time.Second + jitterWaitInterval/2, + }, + }, + { + "4 minutes, no error", + 4 * time.Minute, + nil, + [2]time.Duration{ + 1*time.Second - jitterWaitInterval/2, + 1*time.Second + jitterWaitInterval/2, + }, + }, + { + "4 minutes, error", + 4 * time.Minute, + errors.New("test"), + [2]time.Duration{ + 5*time.Minute - jitterWaitInterval/2, + 5*time.Minute + jitterWaitInterval/2, + }, + }, + { + "5 minutes, no error", + 5 * time.Minute, + nil, + [2]time.Duration{ + 1*time.Second - jitterWaitInterval/2, + 1*time.Second + jitterWaitInterval/2, + }, + }, + { + "5 minutes, error", + 5 * time.Minute, + errors.New("test"), + [2]time.Duration{ + 5*time.Minute - jitterWaitInterval/2, + 5*time.Minute + jitterWaitInterval/2, + }, + }, + { + "10minutes, no error", + 10 * time.Minute, + nil, + [2]time.Duration{ + 1*time.Second - jitterWaitInterval/2, + 1*time.Second + jitterWaitInterval/2, + }, + }, + { + "10minutes, error", + 10 * time.Minute, + errors.New("test"), + [2]time.Duration{ + 5*time.Minute - jitterWaitInterval/2, + 5*time.Minute + jitterWaitInterval/2, + }, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.Description, func(t *testing.T) { + actualWaitInterval := nextWaitInterval( + testCase.lastWaitInterval, + testCase.err, + ) + assert.GreaterOrEqual(t, int64(actualWaitInterval), int64(testCase.expectedRange[0])) + assert.LessOrEqual(t, int64(actualWaitInterval), int64(testCase.expectedRange[1])) + }) + } +} diff --git a/server/public/pluginapi/cluster_test.go b/server/public/pluginapi/cluster_test.go new file mode 100644 index 00000000000..85348f24129 --- /dev/null +++ b/server/public/pluginapi/cluster_test.go @@ -0,0 +1,48 @@ +package pluginapi_test + +import ( + "errors" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/mattermost/mattermost/server/public/model" + "github.com/mattermost/mattermost/server/public/plugin/plugintest" + "github.com/mattermost/mattermost/server/public/pluginapi" +) + +func TestPublishPluginClusterEvent(t *testing.T) { + t.Run("success", func(t *testing.T) { + api := &plugintest.API{} + defer api.AssertExpectations(t) + client := pluginapi.NewClient(api, &plugintest.Driver{}) + + api.On("PublishPluginClusterEvent", + model.PluginClusterEvent{Id: "someID", Data: []byte("foo")}, + model.PluginClusterEventSendOptions{SendType: model.PluginClusterEventSendTypeReliable}, + ).Return(nil) + + err := client.Cluster.PublishPluginEvent( + model.PluginClusterEvent{Id: "someID", Data: []byte("foo")}, + model.PluginClusterEventSendOptions{SendType: model.PluginClusterEventSendTypeReliable}, + ) + require.NoError(t, err) + }) + + t.Run("failure", func(t *testing.T) { + api := &plugintest.API{} + defer api.AssertExpectations(t) + client := pluginapi.NewClient(api, &plugintest.Driver{}) + + api.On("PublishPluginClusterEvent", + model.PluginClusterEvent{Id: "someID", Data: []byte("foo")}, + model.PluginClusterEventSendOptions{SendType: model.PluginClusterEventSendTypeReliable}, + ).Return(errors.New("someError")) + + err := client.Cluster.PublishPluginEvent( + model.PluginClusterEvent{Id: "someID", Data: []byte("foo")}, + model.PluginClusterEventSendOptions{SendType: model.PluginClusterEventSendTypeReliable}, + ) + require.Error(t, err) + }) +} diff --git a/server/public/pluginapi/configuration.go b/server/public/pluginapi/configuration.go new file mode 100644 index 00000000000..f28cfe55e85 --- /dev/null +++ b/server/public/pluginapi/configuration.go @@ -0,0 +1,55 @@ +package pluginapi + +import ( + "github.com/mattermost/mattermost/server/public/model" + "github.com/mattermost/mattermost/server/public/plugin" +) + +// ConfigurationService exposes methods to manipulate the server and plugin configuration. +type ConfigurationService struct { + api plugin.API +} + +// LoadPluginConfiguration loads the plugin's configuration. dest should be a pointer to a +// struct to which the configuration JSON can be unmarshalled. +// +// Minimum server version: 5.2 +func (c *ConfigurationService) LoadPluginConfiguration(dest interface{}) error { + // TODO: Isn't this method redundant given GetPluginConfig() and even GetConfig()? + return c.api.LoadPluginConfiguration(dest) +} + +// GetConfig fetches the currently persisted config. +// +// Minimum server version: 5.2 +func (c *ConfigurationService) GetConfig() *model.Config { + return c.api.GetConfig() +} + +// GetUnsanitizedConfig fetches the currently persisted config without removing secrets. +// +// Minimum server version: 5.16 +func (c *ConfigurationService) GetUnsanitizedConfig() *model.Config { + return c.api.GetUnsanitizedConfig() +} + +// SaveConfig sets the given config and persists the changes +// +// Minimum server version: 5.2 +func (c *ConfigurationService) SaveConfig(cfg *model.Config) error { + return normalizeAppErr(c.api.SaveConfig(cfg)) +} + +// GetPluginConfig fetches the currently persisted config of plugin +// +// Minimum server version: 5.6 +func (c *ConfigurationService) GetPluginConfig() map[string]interface{} { + return c.api.GetPluginConfig() +} + +// SavePluginConfig sets the given config for plugin and persists the changes +// +// Minimum server version: 5.6 +func (c *ConfigurationService) SavePluginConfig(cfg map[string]interface{}) error { + return normalizeAppErr(c.api.SavePluginConfig(cfg)) +} diff --git a/server/public/pluginapi/email.go b/server/public/pluginapi/email.go new file mode 100644 index 00000000000..80a4d97c6e2 --- /dev/null +++ b/server/public/pluginapi/email.go @@ -0,0 +1,17 @@ +package pluginapi + +import ( + "github.com/mattermost/mattermost/server/public/plugin" +) + +// MailService exposes methods to send email. +type MailService struct { + api plugin.API +} + +// Send sends an email to a specific address. +// +// Minimum server version: 5.7 +func (m *MailService) Send(to, subject, htmlBody string) error { + return normalizeAppErr(m.api.SendMail(to, subject, htmlBody)) +} diff --git a/server/public/pluginapi/emoji.go b/server/public/pluginapi/emoji.go new file mode 100644 index 00000000000..ec21de80ae0 --- /dev/null +++ b/server/public/pluginapi/emoji.go @@ -0,0 +1,54 @@ +package pluginapi + +import ( + "bytes" + "io" + + "github.com/mattermost/mattermost/server/public/model" + "github.com/mattermost/mattermost/server/public/plugin" +) + +// EmojiService exposes methods to manipulate emojis. +type EmojiService struct { + api plugin.API +} + +// Get gets a custom emoji by id. +// +// Minimum server version: 5.6 +func (e *EmojiService) Get(id string) (*model.Emoji, error) { + emoji, appErr := e.api.GetEmoji(id) + + return emoji, normalizeAppErr(appErr) +} + +// GetByName gets a custom emoji by its name. +// +// Minimum server version: 5.6 +func (e *EmojiService) GetByName(name string) (*model.Emoji, error) { + emoji, appErr := e.api.GetEmojiByName(name) + + return emoji, normalizeAppErr(appErr) +} + +// GetImage gets a custom emoji's content and format by id. +// +// Minimum server version: 5.6 +func (e *EmojiService) GetImage(id string) (io.Reader, string, error) { + contentBytes, format, appErr := e.api.GetEmojiImage(id) + if appErr != nil { + return nil, "", normalizeAppErr(appErr) + } + + return bytes.NewReader(contentBytes), format, nil +} + +// List retrieves a list of custom emojis. +// sortBy parameter can be: "name". +// +// Minimum server version: 5.6 +func (e *EmojiService) List(sortBy string, page, count int) ([]*model.Emoji, error) { + emojis, appErr := e.api.GetEmojiList(sortBy, page, count) + + return emojis, normalizeAppErr(appErr) +} diff --git a/server/public/pluginapi/emoji_test.go b/server/public/pluginapi/emoji_test.go new file mode 100644 index 00000000000..8bbe8e37d9d --- /dev/null +++ b/server/public/pluginapi/emoji_test.go @@ -0,0 +1,131 @@ +package pluginapi_test + +import ( + "io" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/mattermost/mattermost/server/public/model" + "github.com/mattermost/mattermost/server/public/plugin/plugintest" + "github.com/mattermost/mattermost/server/public/pluginapi" +) + +func TestGetEmoji(t *testing.T) { + t.Run("success", func(t *testing.T) { + api := &plugintest.API{} + defer api.AssertExpectations(t) + client := pluginapi.NewClient(api, &plugintest.Driver{}) + + api.On("GetEmoji", "1").Return(&model.Emoji{Id: "2"}, nil) + + emoji, err := client.Emoji.Get("1") + require.NoError(t, err) + require.Equal(t, "2", emoji.Id) + }) + + t.Run("failure", func(t *testing.T) { + api := &plugintest.API{} + defer api.AssertExpectations(t) + client := pluginapi.NewClient(api, &plugintest.Driver{}) + + appErr := newAppError() + + api.On("GetEmoji", "1").Return(nil, appErr) + + emoji, err := client.Emoji.Get("1") + require.Equal(t, appErr, err) + require.Zero(t, emoji) + }) +} + +func TestGetEmojiByName(t *testing.T) { + t.Run("success", func(t *testing.T) { + api := &plugintest.API{} + defer api.AssertExpectations(t) + client := pluginapi.NewClient(api, &plugintest.Driver{}) + + api.On("GetEmojiByName", "1").Return(&model.Emoji{Id: "2"}, nil) + + emoji, err := client.Emoji.GetByName("1") + require.NoError(t, err) + require.Equal(t, "2", emoji.Id) + }) + + t.Run("failure", func(t *testing.T) { + api := &plugintest.API{} + defer api.AssertExpectations(t) + client := pluginapi.NewClient(api, &plugintest.Driver{}) + + appErr := newAppError() + + api.On("GetEmojiByName", "1").Return(nil, appErr) + + emoji, err := client.Emoji.GetByName("1") + require.Equal(t, appErr, err) + require.Zero(t, emoji) + }) +} + +func TestGetEmojiImage(t *testing.T) { + t.Run("success", func(t *testing.T) { + api := &plugintest.API{} + defer api.AssertExpectations(t) + client := pluginapi.NewClient(api, &plugintest.Driver{}) + + api.On("GetEmojiImage", "1").Return([]byte{1}, "jpg", nil) + + content, format, err := client.Emoji.GetImage("1") + require.NoError(t, err) + contentBytes, err := io.ReadAll(content) + require.NoError(t, err) + require.Equal(t, []byte{1}, contentBytes) + require.Equal(t, "jpg", format) + }) + + t.Run("failure", func(t *testing.T) { + api := &plugintest.API{} + defer api.AssertExpectations(t) + client := pluginapi.NewClient(api, &plugintest.Driver{}) + + appErr := newAppError() + + api.On("GetEmojiImage", "1").Return(nil, "", appErr) + + content, format, err := client.Emoji.GetImage("1") + require.Equal(t, appErr, err) + require.Zero(t, content) + require.Zero(t, format) + }) +} + +func TestListEmojis(t *testing.T) { + t.Run("success", func(t *testing.T) { + api := &plugintest.API{} + defer api.AssertExpectations(t) + client := pluginapi.NewClient(api, &plugintest.Driver{}) + + api.On("GetEmojiList", "1", 2, 3).Return([]*model.Emoji{ + {Id: "4"}, + }, nil) + + emojis, err := client.Emoji.List("1", 2, 3) + require.NoError(t, err) + require.Len(t, emojis, 1) + require.Equal(t, "4", emojis[0].Id) + }) + + t.Run("failure", func(t *testing.T) { + api := &plugintest.API{} + defer api.AssertExpectations(t) + client := pluginapi.NewClient(api, &plugintest.Driver{}) + + appErr := newAppError() + + api.On("GetEmojiList", "1", 2, 3).Return(nil, appErr) + + emojis, err := client.Emoji.List("1", 2, 3) + require.Equal(t, appErr, err) + require.Zero(t, emojis) + }) +} diff --git a/server/public/pluginapi/error.go b/server/public/pluginapi/error.go new file mode 100644 index 00000000000..32765915ef6 --- /dev/null +++ b/server/public/pluginapi/error.go @@ -0,0 +1,39 @@ +package pluginapi + +import ( + "net/http" + + "github.com/pkg/errors" + + "github.com/mattermost/mattermost/server/public/model" +) + +// ErrNotFound is returned by the plugin API when an object is not found. +var ErrNotFound = errors.New("not found") + +// normalizeAppErr returns a truly nil error if appErr is nil as well as normalizing a class +// of non-nil AppErrors to simplify use within plugins. +// +// This doesn't happen automatically when a *model.AppError is cast to an error, since the +// resulting error interface has a concrete type with a nil value. This leads to the seemingly +// impossible: +// +// var err error +// err = func() *model.AppError { return nil }() +// if err != nil { +// panic("err != nil, which surprises most") +// } +// +// Fix this problem for all plugin authors by normalizing to special case the handling of a nil +// *model.AppError. See https://golang.org/doc/faq#nil_error for more details. +func normalizeAppErr(appErr *model.AppError) error { + if appErr == nil { + return nil + } + + if appErr.StatusCode == http.StatusNotFound { + return ErrNotFound + } + + return appErr +} diff --git a/server/public/pluginapi/example_client_test.go b/server/public/pluginapi/example_client_test.go new file mode 100644 index 00000000000..c254c370bda --- /dev/null +++ b/server/public/pluginapi/example_client_test.go @@ -0,0 +1,21 @@ +package pluginapi_test + +import ( + "github.com/mattermost/mattermost/server/public/pluginapi" + + "github.com/mattermost/mattermost/server/public/plugin" +) + +type Plugin struct { + plugin.MattermostPlugin + client *pluginapi.Client +} + +func (p *Plugin) OnActivate() error { + p.client = pluginapi.NewClient(p.API, p.Driver) + + return nil +} + +func Example() { +} diff --git a/server/public/pluginapi/experimental/bot/bot.go b/server/public/pluginapi/experimental/bot/bot.go new file mode 100644 index 00000000000..76634866e53 --- /dev/null +++ b/server/public/pluginapi/experimental/bot/bot.go @@ -0,0 +1,53 @@ +// Copyright (c) 2019-present Mattermost, Inc. All Rights Reserved. +// See License for license information. + +package bot + +import ( + "github.com/pkg/errors" + + "github.com/mattermost/mattermost/server/public/model" + "github.com/mattermost/mattermost/server/public/pluginapi" +) + +type Bot interface { + Ensure(stored *model.Bot, iconPath string) error + MattermostUserID() string + String() string +} + +type bot struct { + botService pluginapi.BotService + mattermostUserID string + displayName string +} + +func New(botService pluginapi.BotService) Bot { + newBot := &bot{ + botService: botService, + } + return newBot +} + +func (bot *bot) Ensure(stored *model.Bot, iconPath string) error { + if bot.mattermostUserID != "" { + // Already done + return nil + } + + botUserID, err := bot.botService.EnsureBot(stored, pluginapi.ProfileImagePath(iconPath)) + if err != nil { + return errors.Wrap(err, "failed to ensure bot account") + } + bot.mattermostUserID = botUserID + bot.displayName = stored.DisplayName + return nil +} + +func (bot *bot) MattermostUserID() string { + return bot.mattermostUserID +} + +func (bot *bot) String() string { + return bot.displayName +} diff --git a/server/public/pluginapi/experimental/bot/logger/admincclogger/admincc_logger.go b/server/public/pluginapi/experimental/bot/logger/admincclogger/admincc_logger.go new file mode 100644 index 00000000000..b3c549e75ef --- /dev/null +++ b/server/public/pluginapi/experimental/bot/logger/admincclogger/admincc_logger.go @@ -0,0 +1,97 @@ +package admincclogger + +import ( + "fmt" + + "github.com/mattermost/mattermost/server/public/pluginapi/experimental/bot/logger" + "github.com/mattermost/mattermost/server/public/pluginapi/experimental/bot/poster" + "github.com/mattermost/mattermost/server/public/pluginapi/experimental/common" +) + +type adminCCLogger struct { + logger.Logger + dmer poster.DMer + logLevel logger.LogLevel + includeContext bool + userIDs []string +} + +/* +New promotes the provided logger into a admin cc logger, sending direct messages to all the admin +ids provided through the dmer provided, about all events below the logLevel. If logVerbose is set, +it will also send the context. + +- l Logger: A logger to promote. + +- dmer DMer: A DMer to send the messages to the admins. + +- logLevel: The highest type of message to be stored in telemetry. + +- includeContext: Whether the log context should be messaged to the admins. + +- userIDs: The user IDs of the admins. +*/ +func New(l logger.Logger, dmer poster.DMer, logLevel logger.LogLevel, includeContext bool, userIDs ...string) logger.Logger { + return &adminCCLogger{ + Logger: l, + dmer: dmer, + logLevel: logLevel, + includeContext: includeContext, + userIDs: userIDs, + } +} + +// NewFromAPI creates a adminCCLogger directly from a LogAPI instead of passing a logger. +func NewFromAPI(api common.LogAPI, dmer poster.DMer, logLevel logger.LogLevel, includeContext bool, userIDs ...string) logger.Logger { + return New(logger.New(api), dmer, logLevel, includeContext, userIDs...) +} + +func (l *adminCCLogger) Debugf(format string, args ...interface{}) { + l.Logger.Debugf(format, args...) + message := fmt.Sprintf(format, args...) + if logger.Level(l.logLevel) >= 4 { + l.logToAdmins("DEBUG", message) + } +} + +func (l *adminCCLogger) Errorf(format string, args ...interface{}) { + l.Logger.Errorf(format, args...) + message := fmt.Sprintf(format, args...) + if logger.Level(l.logLevel) >= 1 { + l.logToAdmins("ERROR", message) + } +} + +func (l *adminCCLogger) Infof(format string, args ...interface{}) { + l.Logger.Infof(format, args...) + message := fmt.Sprintf(format, args...) + if logger.Level(l.logLevel) >= 3 { + l.logToAdmins("INFO", message) + } +} + +func (l *adminCCLogger) Warnf(format string, args ...interface{}) { + l.Logger.Warnf(format, args...) + message := fmt.Sprintf(format, args...) + if logger.Level(l.logLevel) >= 2 { + l.logToAdmins("WARN", message) + } +} + +func (l *adminCCLogger) logToAdmins(level, message string) { + context := l.Context() + if l.includeContext && len(context) > 0 { + message += "\n" + common.JSONBlock(context) + } + _ = l.dmAdmins("(log " + level + ") " + message) +} + +func (l *adminCCLogger) dmAdmins(format string, args ...interface{}) error { + for _, id := range l.userIDs { + _, err := l.dmer.DM(id, format, args) + if err != nil { + return err + } + } + return nil +} diff --git a/server/public/pluginapi/experimental/bot/logger/default_logger.go b/server/public/pluginapi/experimental/bot/logger/default_logger.go new file mode 100644 index 00000000000..e51adf80692 --- /dev/null +++ b/server/public/pluginapi/experimental/bot/logger/default_logger.go @@ -0,0 +1,82 @@ +// Copyright (c) 2019-present Mattermost, Inc. All Rights Reserved. +// See License for license information. + +package logger + +import ( + "fmt" + "time" + + "github.com/mattermost/mattermost/server/public/pluginapi/experimental/common" +) + +type defaultLogger struct { + logContext LogContext + logAPI common.LogAPI +} + +/* +New creates a new logger. + +- api: LogAPI implementation +*/ +func New(api common.LogAPI) Logger { + l := &defaultLogger{ + logAPI: api, + } + return l +} + +func (l *defaultLogger) With(logContext LogContext) Logger { + newLogger := *l + if len(newLogger.logContext) == 0 { + newLogger.logContext = map[string]interface{}{} + } + for k, v := range logContext { + newLogger.logContext[k] = v + } + return &newLogger +} + +func (l *defaultLogger) WithError(err error) Logger { + newLogger := *l + if len(newLogger.logContext) == 0 { + newLogger.logContext = map[string]interface{}{} + } + newLogger.logContext[ErrorKey] = err.Error() + return &newLogger +} + +func (l *defaultLogger) Context() LogContext { + return l.logContext +} + +func (l *defaultLogger) Timed() Logger { + return l.With(LogContext{ + timed: time.Now(), + }) +} + +func (l *defaultLogger) Debugf(format string, args ...interface{}) { + measure(l.logContext) + message := fmt.Sprintf(format, args...) + l.logAPI.LogDebug(message, toKeyValuePairs(l.logContext)...) +} + +func (l *defaultLogger) Errorf(format string, args ...interface{}) { + measure(l.logContext) + message := fmt.Sprintf(format, args...) + l.logAPI.LogError(message, toKeyValuePairs(l.logContext)...) +} + +func (l *defaultLogger) Infof(format string, args ...interface{}) { + measure(l.logContext) + message := fmt.Sprintf(format, args...) + l.logAPI.LogInfo(message, toKeyValuePairs(l.logContext)...) +} + +func (l *defaultLogger) Warnf(format string, args ...interface{}) { + measure(l.logContext) + message := fmt.Sprintf(format, args...) + l.logAPI.LogWarn(message, toKeyValuePairs(l.logContext)...) +} diff --git a/server/public/pluginapi/experimental/bot/logger/logger.go b/server/public/pluginapi/experimental/bot/logger/logger.go new file mode 100644 index 00000000000..71e3924c1bc --- /dev/null +++ b/server/public/pluginapi/experimental/bot/logger/logger.go @@ -0,0 +1,78 @@ +package logger + +import "time" + +const ( + timed = "__since" + elapsed = "Elapsed" + + ErrorKey = "error" +) + +// LogLevel defines the level of log messages +type LogLevel string + +const ( + // LogLevelDebug denotes debug messages + LogLevelDebug = "debug" + // LogLevelInfo denotes info messages + LogLevelInfo = "info" + // LogLevelWarn denotes warn messages + LogLevelWarn = "warn" + // LogLevelError denotes error messages + LogLevelError = "error" +) + +// LogContext defines the context for the logs. +type LogContext map[string]interface{} + +// Logger defines an object able to log messages. +type Logger interface { + // With adds a logContext to the logger. + With(LogContext) Logger + // WithError adds an Error to the logger. + WithError(error) Logger + // Context returns the current context + Context() LogContext + // Timed add a timed log context. + Timed() Logger + // Debugf logs a formatted string as a debug message. + Debugf(format string, args ...interface{}) + // Errorf logs a formatted string as an error message. + Errorf(format string, args ...interface{}) + // Infof logs a formatted string as an info message. + Infof(format string, args ...interface{}) + // Warnf logs a formatted string as an warning message. + Warnf(format string, args ...interface{}) +} + +func measure(lc LogContext) { + if lc[timed] == nil { + return + } + started := lc[timed].(time.Time) + lc[elapsed] = time.Since(started).String() + delete(lc, timed) +} + +// Level assigns an integer to the LogLevel string +func Level(l LogLevel) int { + switch l { + case LogLevelDebug: + return 4 + case LogLevelInfo: + return 3 + case LogLevelWarn: + return 2 + case LogLevelError: + return 1 + } + return 0 +} + +func toKeyValuePairs(in map[string]interface{}) (out []interface{}) { + for k, v := range in { + out = append(out, k, v) + } + return out +} diff --git a/server/public/pluginapi/experimental/bot/logger/nil_logger.go b/server/public/pluginapi/experimental/bot/logger/nil_logger.go new file mode 100644 index 00000000000..45beac83c2c --- /dev/null +++ b/server/public/pluginapi/experimental/bot/logger/nil_logger.go @@ -0,0 +1,17 @@ +package logger + +type nilLogger struct{} + +// NewNilLogger returns a logger that performs no action. +func NewNilLogger() Logger { + return &nilLogger{} +} + +func (l *nilLogger) With(LogContext) Logger { return l } +func (l *nilLogger) WithError(error) Logger { return l } +func (l *nilLogger) Context() LogContext { return nil } +func (l *nilLogger) Timed() Logger { return l } +func (l *nilLogger) Debugf(string, ...interface{}) {} +func (l *nilLogger) Errorf(string, ...interface{}) {} +func (l *nilLogger) Infof(string, ...interface{}) {} +func (l *nilLogger) Warnf(string, ...interface{}) {} diff --git a/server/public/pluginapi/experimental/bot/logger/telemetrylogger/telemetry_logger.go b/server/public/pluginapi/experimental/bot/logger/telemetrylogger/telemetry_logger.go new file mode 100644 index 00000000000..8f6c802c8bd --- /dev/null +++ b/server/public/pluginapi/experimental/bot/logger/telemetrylogger/telemetry_logger.go @@ -0,0 +1,80 @@ +package telemetrylogger + +import ( + "fmt" + + "github.com/mattermost/mattermost/server/public/pluginapi/experimental/bot/logger" + "github.com/mattermost/mattermost/server/public/pluginapi/experimental/common" + "github.com/mattermost/mattermost/server/public/pluginapi/experimental/telemetry" +) + +type telemetryLogger struct { + logger.Logger + logLevel logger.LogLevel + tracker telemetry.Tracker +} + +/* +New promotes the provided logger into a telemetry logger, storing all events below the logLevel +through the tracker. + +- l Logger: A logger to promote. + +- logLevel: The highest type of message to be stored in telemetry. + +- tracker: The telemetry tracker to store the messages. +*/ +func New(l logger.Logger, logLevel logger.LogLevel, tracker telemetry.Tracker) logger.Logger { + return &telemetryLogger{ + Logger: l, + logLevel: logLevel, + tracker: tracker, + } +} + +// NewFromAPI creates a telemetryLogger directly from a LogAPI instead of passing a logger. +func NewFromAPI(api common.LogAPI, logLevel logger.LogLevel, tracker telemetry.Tracker) logger.Logger { + return New(logger.New(api), logLevel, tracker) +} + +func (l *telemetryLogger) Debugf(format string, args ...interface{}) { + l.Logger.Debugf(format, args...) + message := fmt.Sprintf(format, args...) + if logger.Level(l.logLevel) >= 4 { + l.logToTelemetry("DEBUG", message) + } +} + +func (l *telemetryLogger) Errorf(format string, args ...interface{}) { + l.Logger.Errorf(format, args...) + message := fmt.Sprintf(format, args...) + if logger.Level(l.logLevel) >= 1 { + l.logToTelemetry("ERROR", message) + } +} + +func (l *telemetryLogger) Infof(format string, args ...interface{}) { + l.Logger.Infof(format, args...) + message := fmt.Sprintf(format, args...) + if logger.Level(l.logLevel) >= 3 { + l.logToTelemetry("INFO", message) + } +} + +func (l *telemetryLogger) Warnf(format string, args ...interface{}) { + l.Logger.Warnf(format, args...) + message := fmt.Sprintf(format, args...) + if logger.Level(l.logLevel) >= 2 { + l.logToTelemetry("WARN", message) + } +} + +func (l *telemetryLogger) logToTelemetry(level, message string) { + properties := map[string]interface{}{} + properties["message"] = message + for k, v := range l.Context() { + properties["context_"+k] = fmt.Sprintf("%v", v) + } + + _ = l.tracker.TrackEvent("logger_"+level, properties) +} diff --git a/server/public/pluginapi/experimental/bot/logger/test_logger.go b/server/public/pluginapi/experimental/bot/logger/test_logger.go new file mode 100644 index 00000000000..afd85f12ab6 --- /dev/null +++ b/server/public/pluginapi/experimental/bot/logger/test_logger.go @@ -0,0 +1,61 @@ +package logger + +import ( + "fmt" + "testing" + "time" +) + +type testLogger struct { + testing.TB + logContext LogContext +} + +// NewTestLogger creates a logger for testing purposes. +func NewTestLogger() Logger { + return &testLogger{} +} + +func (l *testLogger) With(logContext LogContext) Logger { + newl := *l + if len(newl.logContext) == 0 { + newl.logContext = map[string]interface{}{} + } + for k, v := range logContext { + newl.logContext[k] = v + } + return &newl +} + +func (l *testLogger) WithError(err error) Logger { + newl := *l + if len(newl.logContext) == 0 { + newl.logContext = map[string]interface{}{} + } + newl.logContext[ErrorKey] = err.Error() + return &newl +} + +func (l *testLogger) Context() LogContext { + return l.logContext +} + +func (l *testLogger) Timed() Logger { + return l.With(LogContext{ + timed: time.Now(), + }) +} + +func (l *testLogger) logf(prefix, format string, args ...interface{}) { + out := fmt.Sprintf(prefix+": "+format, args...) + if len(l.logContext) > 0 { + measure(l.logContext) + out += fmt.Sprintf(" -- %+v", l.logContext) + } + l.TB.Logf(out) +} + +func (l *testLogger) Debugf(format string, args ...interface{}) { l.logf("DEBUG", format, args...) } +func (l *testLogger) Errorf(format string, args ...interface{}) { l.logf("ERROR", format, args...) } +func (l *testLogger) Infof(format string, args ...interface{}) { l.logf("INFO", format, args...) } +func (l *testLogger) Warnf(format string, args ...interface{}) { l.logf("WARN", format, args...) } diff --git a/server/public/pluginapi/experimental/bot/mocks/mock_bot.go b/server/public/pluginapi/experimental/bot/mocks/mock_bot.go new file mode 100644 index 00000000000..9ff7b4117c7 --- /dev/null +++ b/server/public/pluginapi/experimental/bot/mocks/mock_bot.go @@ -0,0 +1,77 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/mattermost/mattermost/server/public/pluginapi/experimental/bot (interfaces: Bot) + +// Package mock_bot is a generated GoMock package. +package mock_bot + +import ( + reflect "reflect" + + gomock "github.com/golang/mock/gomock" + model "github.com/mattermost/mattermost/server/public/model" +) + +// MockBot is a mock of Bot interface. +type MockBot struct { + ctrl *gomock.Controller + recorder *MockBotMockRecorder +} + +// MockBotMockRecorder is the mock recorder for MockBot. +type MockBotMockRecorder struct { + mock *MockBot +} + +// NewMockBot creates a new mock instance. +func NewMockBot(ctrl *gomock.Controller) *MockBot { + mock := &MockBot{ctrl: ctrl} + mock.recorder = &MockBotMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockBot) EXPECT() *MockBotMockRecorder { + return m.recorder +} + +// Ensure mocks base method. +func (m *MockBot) Ensure(arg0 *model.Bot, arg1 string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Ensure", arg0, arg1) + ret0, _ := ret[0].(error) + return ret0 +} + +// Ensure indicates an expected call of Ensure. +func (mr *MockBotMockRecorder) Ensure(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Ensure", reflect.TypeOf((*MockBot)(nil).Ensure), arg0, arg1) +} + +// MattermostUserID mocks base method. +func (m *MockBot) MattermostUserID() string { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "MattermostUserID") + ret0, _ := ret[0].(string) + return ret0 +} + +// MattermostUserID indicates an expected call of MattermostUserID. +func (mr *MockBotMockRecorder) MattermostUserID() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "MattermostUserID", reflect.TypeOf((*MockBot)(nil).MattermostUserID)) +} + +// String mocks base method. +func (m *MockBot) String() string { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "String") + ret0, _ := ret[0].(string) + return ret0 +} + +// String indicates an expected call of String. +func (mr *MockBotMockRecorder) String() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "String", reflect.TypeOf((*MockBot)(nil).String)) +} diff --git a/server/public/pluginapi/experimental/bot/mocks/mock_logger.go b/server/public/pluginapi/experimental/bot/mocks/mock_logger.go new file mode 100644 index 00000000000..f096bdae3db --- /dev/null +++ b/server/public/pluginapi/experimental/bot/mocks/mock_logger.go @@ -0,0 +1,159 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/mattermost/mattermost/server/public/pluginapi/experimental/bot/logger (interfaces: Logger) + +// Package mock_bot is a generated GoMock package. +package mock_bot + +import ( + reflect "reflect" + + gomock "github.com/golang/mock/gomock" + logger "github.com/mattermost/mattermost/server/public/pluginapi/experimental/bot/logger" +) + +// MockLogger is a mock of Logger interface. +type MockLogger struct { + ctrl *gomock.Controller + recorder *MockLoggerMockRecorder +} + +// MockLoggerMockRecorder is the mock recorder for MockLogger. +type MockLoggerMockRecorder struct { + mock *MockLogger +} + +// NewMockLogger creates a new mock instance. +func NewMockLogger(ctrl *gomock.Controller) *MockLogger { + mock := &MockLogger{ctrl: ctrl} + mock.recorder = &MockLoggerMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockLogger) EXPECT() *MockLoggerMockRecorder { + return m.recorder +} + +// Context mocks base method. +func (m *MockLogger) Context() logger.LogContext { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Context") + ret0, _ := ret[0].(logger.LogContext) + return ret0 +} + +// Context indicates an expected call of Context. +func (mr *MockLoggerMockRecorder) Context() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Context", reflect.TypeOf((*MockLogger)(nil).Context)) +} + +// Debugf mocks base method. +func (m *MockLogger) Debugf(arg0 string, arg1 ...interface{}) { + m.ctrl.T.Helper() + varargs := []interface{}{arg0} + for _, a := range arg1 { + varargs = append(varargs, a) + } + m.ctrl.Call(m, "Debugf", varargs...) +} + +// Debugf indicates an expected call of Debugf. +func (mr *MockLoggerMockRecorder) Debugf(arg0 interface{}, arg1 ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{arg0}, arg1...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Debugf", reflect.TypeOf((*MockLogger)(nil).Debugf), varargs...) +} + +// Errorf mocks base method. +func (m *MockLogger) Errorf(arg0 string, arg1 ...interface{}) { + m.ctrl.T.Helper() + varargs := []interface{}{arg0} + for _, a := range arg1 { + varargs = append(varargs, a) + } + m.ctrl.Call(m, "Errorf", varargs...) +} + +// Errorf indicates an expected call of Errorf. +func (mr *MockLoggerMockRecorder) Errorf(arg0 interface{}, arg1 ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{arg0}, arg1...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Errorf", reflect.TypeOf((*MockLogger)(nil).Errorf), varargs...) +} + +// Infof mocks base method. +func (m *MockLogger) Infof(arg0 string, arg1 ...interface{}) { + m.ctrl.T.Helper() + varargs := []interface{}{arg0} + for _, a := range arg1 { + varargs = append(varargs, a) + } + m.ctrl.Call(m, "Infof", varargs...) +} + +// Infof indicates an expected call of Infof. +func (mr *MockLoggerMockRecorder) Infof(arg0 interface{}, arg1 ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{arg0}, arg1...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Infof", reflect.TypeOf((*MockLogger)(nil).Infof), varargs...) +} + +// Timed mocks base method. +func (m *MockLogger) Timed() logger.Logger { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Timed") + ret0, _ := ret[0].(logger.Logger) + return ret0 +} + +// Timed indicates an expected call of Timed. +func (mr *MockLoggerMockRecorder) Timed() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Timed", reflect.TypeOf((*MockLogger)(nil).Timed)) +} + +// Warnf mocks base method. +func (m *MockLogger) Warnf(arg0 string, arg1 ...interface{}) { + m.ctrl.T.Helper() + varargs := []interface{}{arg0} + for _, a := range arg1 { + varargs = append(varargs, a) + } + m.ctrl.Call(m, "Warnf", varargs...) +} + +// Warnf indicates an expected call of Warnf. +func (mr *MockLoggerMockRecorder) Warnf(arg0 interface{}, arg1 ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{arg0}, arg1...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Warnf", reflect.TypeOf((*MockLogger)(nil).Warnf), varargs...) +} + +// With mocks base method. +func (m *MockLogger) With(arg0 logger.LogContext) logger.Logger { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "With", arg0) + ret0, _ := ret[0].(logger.Logger) + return ret0 +} + +// With indicates an expected call of With. +func (mr *MockLoggerMockRecorder) With(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "With", reflect.TypeOf((*MockLogger)(nil).With), arg0) +} + +// WithError mocks base method. +func (m *MockLogger) WithError(arg0 error) logger.Logger { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "WithError", arg0) + ret0, _ := ret[0].(logger.Logger) + return ret0 +} + +// WithError indicates an expected call of WithError. +func (mr *MockLoggerMockRecorder) WithError(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "WithError", reflect.TypeOf((*MockLogger)(nil).WithError), arg0) +} diff --git a/server/public/pluginapi/experimental/bot/mocks/mock_poster.go b/server/public/pluginapi/experimental/bot/mocks/mock_poster.go new file mode 100644 index 00000000000..c826fb5ad97 --- /dev/null +++ b/server/public/pluginapi/experimental/bot/mocks/mock_poster.go @@ -0,0 +1,151 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/mattermost/mattermost/server/public/pluginapi/experimental/bot/poster (interfaces: Poster) + +// Package mock_bot is a generated GoMock package. +package mock_bot + +import ( + reflect "reflect" + + gomock "github.com/golang/mock/gomock" + model "github.com/mattermost/mattermost/server/public/model" +) + +// MockPoster is a mock of Poster interface. +type MockPoster struct { + ctrl *gomock.Controller + recorder *MockPosterMockRecorder +} + +// MockPosterMockRecorder is the mock recorder for MockPoster. +type MockPosterMockRecorder struct { + mock *MockPoster +} + +// NewMockPoster creates a new mock instance. +func NewMockPoster(ctrl *gomock.Controller) *MockPoster { + mock := &MockPoster{ctrl: ctrl} + mock.recorder = &MockPosterMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockPoster) EXPECT() *MockPosterMockRecorder { + return m.recorder +} + +// DM mocks base method. +func (m *MockPoster) DM(arg0, arg1 string, arg2 ...interface{}) (string, error) { + m.ctrl.T.Helper() + varargs := []interface{}{arg0, arg1} + for _, a := range arg2 { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "DM", varargs...) + ret0, _ := ret[0].(string) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// DM indicates an expected call of DM. +func (mr *MockPosterMockRecorder) DM(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{arg0, arg1}, arg2...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DM", reflect.TypeOf((*MockPoster)(nil).DM), varargs...) +} + +// DMWithAttachments mocks base method. +func (m *MockPoster) DMWithAttachments(arg0 string, arg1 ...*model.SlackAttachment) (string, error) { + m.ctrl.T.Helper() + varargs := []interface{}{arg0} + for _, a := range arg1 { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "DMWithAttachments", varargs...) + ret0, _ := ret[0].(string) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// DMWithAttachments indicates an expected call of DMWithAttachments. +func (mr *MockPosterMockRecorder) DMWithAttachments(arg0 interface{}, arg1 ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{arg0}, arg1...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DMWithAttachments", reflect.TypeOf((*MockPoster)(nil).DMWithAttachments), varargs...) +} + +// DeletePost mocks base method. +func (m *MockPoster) DeletePost(arg0 string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DeletePost", arg0) + ret0, _ := ret[0].(error) + return ret0 +} + +// DeletePost indicates an expected call of DeletePost. +func (mr *MockPosterMockRecorder) DeletePost(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeletePost", reflect.TypeOf((*MockPoster)(nil).DeletePost), arg0) +} + +// Ephemeral mocks base method. +func (m *MockPoster) Ephemeral(arg0, arg1, arg2 string, arg3 ...interface{}) { + m.ctrl.T.Helper() + varargs := []interface{}{arg0, arg1, arg2} + for _, a := range arg3 { + varargs = append(varargs, a) + } + m.ctrl.Call(m, "Ephemeral", varargs...) +} + +// Ephemeral indicates an expected call of Ephemeral. +func (mr *MockPosterMockRecorder) Ephemeral(arg0, arg1, arg2 interface{}, arg3 ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{arg0, arg1, arg2}, arg3...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Ephemeral", reflect.TypeOf((*MockPoster)(nil).Ephemeral), varargs...) +} + +// UpdatePost mocks base method. +func (m *MockPoster) UpdatePost(arg0 *model.Post) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UpdatePost", arg0) + ret0, _ := ret[0].(error) + return ret0 +} + +// UpdatePost indicates an expected call of UpdatePost. +func (mr *MockPosterMockRecorder) UpdatePost(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdatePost", reflect.TypeOf((*MockPoster)(nil).UpdatePost), arg0) +} + +// UpdatePostByID mocks base method. +func (m *MockPoster) UpdatePostByID(arg0, arg1 string, arg2 ...interface{}) error { + m.ctrl.T.Helper() + varargs := []interface{}{arg0, arg1} + for _, a := range arg2 { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "UpdatePostByID", varargs...) + ret0, _ := ret[0].(error) + return ret0 +} + +// UpdatePostByID indicates an expected call of UpdatePostByID. +func (mr *MockPosterMockRecorder) UpdatePostByID(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{arg0, arg1}, arg2...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdatePostByID", reflect.TypeOf((*MockPoster)(nil).UpdatePostByID), varargs...) +} + +// UpdatePosterID mocks base method. +func (m *MockPoster) UpdatePosterID(arg0 string) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "UpdatePosterID", arg0) +} + +// UpdatePosterID indicates an expected call of UpdatePosterID. +func (mr *MockPosterMockRecorder) UpdatePosterID(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdatePosterID", reflect.TypeOf((*MockPoster)(nil).UpdatePosterID), arg0) +} diff --git a/server/public/pluginapi/experimental/bot/poster/default_poster.go b/server/public/pluginapi/experimental/bot/poster/default_poster.go new file mode 100644 index 00000000000..16b9f45c2bb --- /dev/null +++ b/server/public/pluginapi/experimental/bot/poster/default_poster.go @@ -0,0 +1,76 @@ +package poster + +import ( + "fmt" + + "github.com/mattermost/mattermost/server/public/model" +) + +type defaultPoster struct { + postAPI PostAPI + id string +} + +// NewPoster creates a new default poster +func NewPoster(postAPI PostAPI, id string) Poster { + return &defaultPoster{ + postAPI: postAPI, + id: id, + } +} + +// DM posts a simple Direct Message to the specified user +func (p *defaultPoster) DM(mattermostUserID, format string, args ...interface{}) (string, error) { + post := &model.Post{ + Message: fmt.Sprintf(format, args...), + } + err := p.postAPI.DM(p.id, mattermostUserID, post) + if err != nil { + return "", err + } + return post.Id, nil +} + +// DMWithAttachments posts a Direct Message that contains Slack attachments. +// Often used to include post actions. +func (p *defaultPoster) DMWithAttachments(mattermostUserID string, attachments ...*model.SlackAttachment) (string, error) { + post := model.Post{} + model.ParseSlackAttachment(&post, attachments) + err := p.postAPI.DM(p.id, mattermostUserID, &post) + if err != nil { + return "", err + } + return post.Id, nil +} + +// Ephemeral sends an ephemeral message to a user +func (p *defaultPoster) Ephemeral(userID, channelID, format string, args ...interface{}) { + post := &model.Post{ + UserId: p.id, + ChannelId: channelID, + Message: fmt.Sprintf(format, args...), + } + p.postAPI.SendEphemeralPost(userID, post) +} + +func (p *defaultPoster) UpdatePostByID(postID, format string, args ...interface{}) error { + post, err := p.postAPI.GetPost(postID) + if err != nil { + return err + } + + post.Message = fmt.Sprintf(format, args...) + return p.UpdatePost(post) +} + +func (p *defaultPoster) DeletePost(postID string) error { + return p.postAPI.DeletePost(postID) +} + +func (p *defaultPoster) UpdatePost(post *model.Post) error { + return p.postAPI.UpdatePost(post) +} + +func (p *defaultPoster) UpdatePosterID(id string) { + p.id = id +} diff --git a/server/public/pluginapi/experimental/bot/poster/default_poster_test.go b/server/public/pluginapi/experimental/bot/poster/default_poster_test.go new file mode 100644 index 00000000000..2770b13f882 --- /dev/null +++ b/server/public/pluginapi/experimental/bot/poster/default_poster_test.go @@ -0,0 +1,414 @@ +package poster + +import ( + "errors" + "testing" + + "github.com/golang/mock/gomock" + "github.com/stretchr/testify/assert" + + "github.com/mattermost/mattermost/server/public/model" + "github.com/mattermost/mattermost/server/public/plugin/plugintest" + "github.com/mattermost/mattermost/server/public/pluginapi" + "github.com/mattermost/mattermost/server/public/pluginapi/experimental/bot/poster/mock_import" +) + +const ( + botID = "test-bot-user" + userID = "test-user-1" + dmChannelID = "dm-channel-id" +) + +func TestInterface(t *testing.T) { + t.Run("Plugin API satisfy the interface", func(t *testing.T) { + api := &plugintest.API{} + driver := &plugintest.Driver{} + client := pluginapi.NewClient(api, driver) + _ = NewPoster(&client.Post, botID) + }) +} + +func TestDM(t *testing.T) { + format := "test format, string: %s int: %d value: %v" + args := []interface{}{"some string", 5, 8.423} + expectedMessage := "test format, string: some string int: 5 value: 8.423" + + expectedPostID := "expected-post-id" + + post := &model.Post{ + Message: expectedMessage, + } + + postWithID := model.Post{ + Id: expectedPostID, + UserId: botID, + ChannelId: dmChannelID, + Message: expectedMessage, + } + + mockError := errors.New("mock error") + + t.Run("DM Success", func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + postAPI := mock_import.NewMockPostAPI(ctrl) + + poster := NewPoster(postAPI, botID) + + //nolint:govet //copy lock, but only used in tests + postAPI. + EXPECT(). + DM(botID, userID, post). + SetArg(2, postWithID). + Return(nil). + Times(1) + + postID, err := poster.DM(userID, format, args...) + assert.Equal(t, expectedPostID, postID) + assert.NoError(t, err) + }) + + t.Run("DM error", func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + postAPI := mock_import.NewMockPostAPI(ctrl) + + poster := NewPoster(postAPI, botID) + + postAPI. + EXPECT(). + DM(botID, userID, post). + Return(mockError). + Times(1) + + _, err := poster.DM(userID, format, args...) + assert.Error(t, err) + }) +} + +func TestDMWithAttachments(t *testing.T) { + expectedPostID := "expected-post-id" + + attachments := []*model.SlackAttachment{ + {}, + {}, + } + + post := &model.Post{} + + model.ParseSlackAttachment(post, attachments) + + postWithID := model.Post{ + Id: expectedPostID, + UserId: botID, + ChannelId: dmChannelID, + Type: model.PostTypeSlackAttachment, + Props: model.StringInterface{ + "attachments": attachments, + }, + } + + mockError := errors.New("mock error") + t.Run("DM Success", func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + postAPI := mock_import.NewMockPostAPI(ctrl) + + poster := NewPoster(postAPI, botID) + + //nolint:govet //copy lock, but only used in tests + postAPI. + EXPECT(). + DM(botID, userID, post). + SetArg(2, postWithID). + Return(nil). + Times(1) + + postID, err := poster.DMWithAttachments(userID, attachments...) + assert.Equal(t, expectedPostID, postID) + assert.NoError(t, err) + }) + + t.Run("DM error", func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + postAPI := mock_import.NewMockPostAPI(ctrl) + + poster := NewPoster(postAPI, botID) + + postAPI. + EXPECT(). + DM(botID, userID, post). + Return(mockError). + Times(1) + + _, err := poster.DMWithAttachments(userID, attachments...) + assert.Error(t, err) + }) +} + +func TestEphemeral(t *testing.T) { + format := "test format, string: %s int: %d value: %v" + args := []interface{}{"some string", 5, 8.423} + expectedMessage := "test format, string: some string int: 5 value: 8.423" + + channelID := "some-channel" + + post := &model.Post{ + UserId: botID, + ChannelId: channelID, + Message: expectedMessage, + } + + expectedPostID := "some-post-ID" + + postWithID := model.Post{ + Id: expectedPostID, + UserId: botID, + ChannelId: channelID, + Message: expectedMessage, + } + + t.Run("Success", func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + postAPI := mock_import.NewMockPostAPI(ctrl) + + poster := NewPoster(postAPI, botID) + + //nolint:govet //copy lock, but only used in tests + postAPI. + EXPECT(). + SendEphemeralPost(userID, post). + SetArg(1, postWithID). + Times(1) + + poster.Ephemeral(userID, channelID, format, args...) + }) +} + +func TestUpdatePostByID(t *testing.T) { + format := "test format, string: %s int: %d value: %v" + args := []interface{}{"some string", 5, 8.423} + expectedMessage := "test format, string: some string int: 5 value: 8.423" + + postID := "some-post-id" + originalPost := &model.Post{ + Id: postID, + Message: "some message", + } + + updatedPost := &model.Post{ + Id: postID, + Message: expectedMessage, + } + + mockError := errors.New("mock error") + + t.Run("Success", func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + postAPI := mock_import.NewMockPostAPI(ctrl) + + poster := NewPoster(postAPI, botID) + + postAPI. + EXPECT(). + GetPost(postID). + Return(originalPost, nil). + Times(1) + + postAPI. + EXPECT(). + UpdatePost(updatedPost). + Return(nil). + Times(1) + + err := poster.UpdatePostByID(postID, format, args...) + assert.NoError(t, err) + }) + + t.Run("Error fetching", func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + postAPI := mock_import.NewMockPostAPI(ctrl) + + poster := NewPoster(postAPI, botID) + + postAPI. + EXPECT(). + GetPost(postID). + Return(nil, mockError). + Times(1) + + err := poster.UpdatePostByID(postID, format, args...) + assert.Error(t, err) + }) + + t.Run("Error updating", func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + postAPI := mock_import.NewMockPostAPI(ctrl) + + poster := NewPoster(postAPI, botID) + + postAPI. + EXPECT(). + GetPost(postID). + Return(originalPost, nil). + Times(1) + + postAPI. + EXPECT(). + UpdatePost(updatedPost). + Return(mockError). + Times(1) + + err := poster.UpdatePostByID(postID, format, args...) + assert.Error(t, err) + }) +} + +func TestDeletePost(t *testing.T) { + postID := "some-post-id" + + mockError := errors.New("mock channel error") + t.Run("Success", func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + postAPI := mock_import.NewMockPostAPI(ctrl) + + poster := NewPoster(postAPI, botID) + + postAPI. + EXPECT(). + DeletePost(postID). + Return(nil). + Times(1) + + err := poster.DeletePost(postID) + assert.NoError(t, err) + }) + + t.Run("Error", func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + postAPI := mock_import.NewMockPostAPI(ctrl) + + poster := NewPoster(postAPI, botID) + + postAPI. + EXPECT(). + DeletePost(postID). + Return(mockError). + Times(1) + + err := poster.DeletePost(postID) + assert.Error(t, err) + }) +} + +func TestUpdatePost(t *testing.T) { + post := &model.Post{ + Id: "some-post-id", + Message: "some message", + } + + mockError := errors.New("mock channel error") + t.Run("Success", func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + postAPI := mock_import.NewMockPostAPI(ctrl) + + poster := NewPoster(postAPI, botID) + + postAPI. + EXPECT(). + UpdatePost(post). + Return(nil). + Times(1) + + err := poster.UpdatePost(post) + assert.NoError(t, err) + }) + + t.Run("Error", func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + postAPI := mock_import.NewMockPostAPI(ctrl) + + poster := NewPoster(postAPI, botID) + + postAPI. + EXPECT(). + UpdatePost(post). + Return(mockError). + Times(1) + + err := poster.UpdatePost(post) + assert.Error(t, err) + }) +} + +func TestUpdatePosterID(t *testing.T) { + format := "test format, string: %s int: %d value: %v" + args := []interface{}{"some string", 5, 8.423} + expectedMessage := "test format, string: some string int: 5 value: 8.423" + + expectedPostID := "expected-post-id" + + post := &model.Post{ + Message: expectedMessage, + } + + postWithID := model.Post{ + Id: expectedPostID, + UserId: botID, + ChannelId: dmChannelID, + Message: expectedMessage, + } + + newBotID := "new-bot-id" + + t.Run("Success", func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + postAPI := mock_import.NewMockPostAPI(ctrl) + + poster := NewPoster(postAPI, botID) + + //nolint:govet //copy lock, but only used in tests + postAPI. + EXPECT(). + DM(botID, userID, post). + SetArg(2, postWithID). + Return(nil). + Times(1) + + _, _ = poster.DM(userID, format, args...) + poster.UpdatePosterID(newBotID) + + //nolint:govet //copy lock, but only used in tests + postAPI. + EXPECT(). + DM(newBotID, userID, post). + SetArg(2, postWithID). + Return(nil). + Times(1) + + _, _ = poster.DM(userID, format, args...) + }) +} diff --git a/server/public/pluginapi/experimental/bot/poster/import.go b/server/public/pluginapi/experimental/bot/poster/import.go new file mode 100644 index 00000000000..f3c532d9702 --- /dev/null +++ b/server/public/pluginapi/experimental/bot/poster/import.go @@ -0,0 +1,14 @@ +package poster + +import ( + "github.com/mattermost/mattermost/server/public/model" +) + +// PostAPI defines the portion of the Post Service used by the poster +type PostAPI interface { + DM(senderUserID, receiverUserID string, post *model.Post) error + GetPost(postID string) (*model.Post, error) + UpdatePost(post *model.Post) error + DeletePost(postID string) error + SendEphemeralPost(userID string, post *model.Post) +} diff --git a/server/public/pluginapi/experimental/bot/poster/mock_import/mock_postapi.go b/server/public/pluginapi/experimental/bot/poster/mock_import/mock_postapi.go new file mode 100644 index 00000000000..7fe3eb21c6c --- /dev/null +++ b/server/public/pluginapi/experimental/bot/poster/mock_import/mock_postapi.go @@ -0,0 +1,104 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/mattermost/mattermost/server/public/pluginapi/experimental/bot/poster (interfaces: PostAPI) + +// Package mock_import is a generated GoMock package. +package mock_import + +import ( + reflect "reflect" + + gomock "github.com/golang/mock/gomock" + model "github.com/mattermost/mattermost/server/public/model" +) + +// MockPostAPI is a mock of PostAPI interface. +type MockPostAPI struct { + ctrl *gomock.Controller + recorder *MockPostAPIMockRecorder +} + +// MockPostAPIMockRecorder is the mock recorder for MockPostAPI. +type MockPostAPIMockRecorder struct { + mock *MockPostAPI +} + +// NewMockPostAPI creates a new mock instance. +func NewMockPostAPI(ctrl *gomock.Controller) *MockPostAPI { + mock := &MockPostAPI{ctrl: ctrl} + mock.recorder = &MockPostAPIMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockPostAPI) EXPECT() *MockPostAPIMockRecorder { + return m.recorder +} + +// DM mocks base method. +func (m *MockPostAPI) DM(arg0, arg1 string, arg2 *model.Post) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DM", arg0, arg1, arg2) + ret0, _ := ret[0].(error) + return ret0 +} + +// DM indicates an expected call of DM. +func (mr *MockPostAPIMockRecorder) DM(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DM", reflect.TypeOf((*MockPostAPI)(nil).DM), arg0, arg1, arg2) +} + +// DeletePost mocks base method. +func (m *MockPostAPI) DeletePost(arg0 string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DeletePost", arg0) + ret0, _ := ret[0].(error) + return ret0 +} + +// DeletePost indicates an expected call of DeletePost. +func (mr *MockPostAPIMockRecorder) DeletePost(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeletePost", reflect.TypeOf((*MockPostAPI)(nil).DeletePost), arg0) +} + +// GetPost mocks base method. +func (m *MockPostAPI) GetPost(arg0 string) (*model.Post, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetPost", arg0) + ret0, _ := ret[0].(*model.Post) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetPost indicates an expected call of GetPost. +func (mr *MockPostAPIMockRecorder) GetPost(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPost", reflect.TypeOf((*MockPostAPI)(nil).GetPost), arg0) +} + +// SendEphemeralPost mocks base method. +func (m *MockPostAPI) SendEphemeralPost(arg0 string, arg1 *model.Post) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "SendEphemeralPost", arg0, arg1) +} + +// SendEphemeralPost indicates an expected call of SendEphemeralPost. +func (mr *MockPostAPIMockRecorder) SendEphemeralPost(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SendEphemeralPost", reflect.TypeOf((*MockPostAPI)(nil).SendEphemeralPost), arg0, arg1) +} + +// UpdatePost mocks base method. +func (m *MockPostAPI) UpdatePost(arg0 *model.Post) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UpdatePost", arg0) + ret0, _ := ret[0].(error) + return ret0 +} + +// UpdatePost indicates an expected call of UpdatePost. +func (mr *MockPostAPIMockRecorder) UpdatePost(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdatePost", reflect.TypeOf((*MockPostAPI)(nil).UpdatePost), arg0) +} diff --git a/server/public/pluginapi/experimental/bot/poster/poster.go b/server/public/pluginapi/experimental/bot/poster/poster.go new file mode 100644 index 00000000000..2fe032a7a33 --- /dev/null +++ b/server/public/pluginapi/experimental/bot/poster/poster.go @@ -0,0 +1,38 @@ +// Copyright (c) 2019-present Mattermost, Inc. All Rights Reserved. +// See License for license information. + +package poster + +import ( + "github.com/mattermost/mattermost/server/public/model" +) + +// Poster defines an entity that can post DMs and Ephemerals and update and delete those posts +type Poster interface { + DMer + + // DMWithAttachments posts a Direct Message that contains Slack attachments. + // Often used to include post actions. + DMWithAttachments(mattermostUserID string, attachments ...*model.SlackAttachment) (string, error) + + // Ephemeral sends an ephemeral message to a user + Ephemeral(mattermostUserID, channelID, format string, args ...interface{}) + + // UpdatePostByID updates the post with postID with the formatted message + UpdatePostByID(postID, format string, args ...interface{}) error + + // DeletePost deletes a single post + DeletePost(postID string) error + + // DMUpdatePost substitute one post with another + UpdatePost(post *model.Post) error + + // UpdatePosterID updates the Mattermost User ID of the poster + UpdatePosterID(id string) +} + +// DMer defines an entity that can send Direct Messages +type DMer interface { + // DM posts a simple Direct Message to the specified user + DM(mattermostUserID, format string, args ...interface{}) (string, error) +} diff --git a/server/public/pluginapi/experimental/command/command.go b/server/public/pluginapi/experimental/command/command.go new file mode 100644 index 00000000000..da8b0ff0bdb --- /dev/null +++ b/server/public/pluginapi/experimental/command/command.go @@ -0,0 +1,31 @@ +package command + +import ( + "encoding/base64" + "fmt" + "os" + "path/filepath" + + "github.com/pkg/errors" +) + +// PluginAPI is the plugin API interface required to manage slash commands. +type PluginAPI interface { + GetBundlePath() (string, error) +} + +// GetIconData returns the base64 encoding of a icon for a given path. +// The data returned may be used for slash command autocomplete. +func GetIconData(api PluginAPI, iconPath string) (string, error) { + bundlePath, err := api.GetBundlePath() + if err != nil { + return "", errors.Wrap(err, "couldn't get bundle path") + } + + icon, err := os.ReadFile(filepath.Join(bundlePath, iconPath)) + if err != nil { + return "", errors.Wrap(err, "failed to open icon") + } + + return fmt.Sprintf("data:image/svg+xml;base64,%s", base64.StdEncoding.EncodeToString(icon)), nil +} diff --git a/server/public/pluginapi/experimental/command/info.go b/server/public/pluginapi/experimental/command/info.go new file mode 100644 index 00000000000..807107f2d20 --- /dev/null +++ b/server/public/pluginapi/experimental/command/info.go @@ -0,0 +1,74 @@ +package command + +import ( + "fmt" + "regexp" + "runtime/debug" + "strings" + "time" + + "github.com/pkg/errors" + + "github.com/mattermost/mattermost/server/public/model" +) + +var versionRegexp = regexp.MustCompile(`/v\d$`) + +func BuildInfoAutocomplete(cmd string) *model.AutocompleteData { + return model.NewAutocompleteData(cmd, "", "Display build info") +} + +func BuildInfo(manifest model.Manifest) (string, error) { + info, ok := debug.ReadBuildInfo() + if !ok { + return "", errors.New("failed to read build info") + } + + var ( + revision string + revisionShort string + buildTime time.Time + dirty bool + ) + for _, s := range info.Settings { + switch s.Key { + case "vcs.revision": + revision = s.Value + revisionShort = revision[0:7] + case "vcs.time": + var err error + buildTime, err = time.Parse(time.RFC3339, s.Value) + + if err != nil { + return "", err + } + case "vcs.modified": + if s.Value == "true" { + dirty = true + } + } + } + + path := info.Main.Path + + matches := versionRegexp.FindAllString(path, -1) + if len(matches) > 0 { + path = strings.TrimSuffix(path, matches[len(matches)-1]) + } + + dirtyText := "" + if dirty { + dirtyText = " (dirty)" + } + + commit := fmt.Sprintf("[%s](https://%s/commit/%s)", revisionShort, path, revision) + + return fmt.Sprintf("%s version: %s, %s%s, built %s with %s\n", + manifest.Name, + manifest.Version, + commit, + dirtyText, + buildTime.Format(time.RFC1123), + info.GoVersion), + nil +} diff --git a/server/public/pluginapi/experimental/common/kvstore.go b/server/public/pluginapi/experimental/common/kvstore.go new file mode 100644 index 00000000000..7e797cffdb1 --- /dev/null +++ b/server/public/pluginapi/experimental/common/kvstore.go @@ -0,0 +1,21 @@ +package common + +import ( + "errors" + "time" + + "github.com/mattermost/mattermost/server/public/pluginapi" +) + +var ErrNotFound = errors.New("not found") + +type KVStore interface { + Set(key string, value interface{}, options ...pluginapi.KVSetOption) (bool, error) + SetWithExpiry(key string, value interface{}, ttl time.Duration) error + CompareAndSet(key string, oldValue, value interface{}) (bool, error) + CompareAndDelete(key string, oldValue interface{}) (bool, error) + Get(key string, o interface{}) error + Delete(key string) error + DeleteAll() error + ListKeys(page, count int, options ...pluginapi.ListKeysOption) ([]string, error) +} diff --git a/server/public/pluginapi/experimental/common/logapi.go b/server/public/pluginapi/experimental/common/logapi.go new file mode 100644 index 00000000000..8613ef59a00 --- /dev/null +++ b/server/public/pluginapi/experimental/common/logapi.go @@ -0,0 +1,8 @@ +package common + +type LogAPI interface { + LogError(message string, keyValuePairs ...interface{}) + LogWarn(message string, keyValuePairs ...interface{}) + LogInfo(message string, keyValuePairs ...interface{}) + LogDebug(message string, keyValuePairs ...interface{}) +} diff --git a/server/public/pluginapi/experimental/common/markdown.go b/server/public/pluginapi/experimental/common/markdown.go new file mode 100644 index 00000000000..34d418dd978 --- /dev/null +++ b/server/public/pluginapi/experimental/common/markdown.go @@ -0,0 +1,22 @@ +// Copyright (c) 2019-present Mattermost, Inc. All Rights Reserved. +// See License for license information. + +package common + +import ( + "encoding/json" + "fmt" +) + +func JSON(ref interface{}) string { + bb, _ := json.MarshalIndent(ref, "", " ") + return string(bb) +} + +func CodeBlock(in string) string { + return fmt.Sprintf("\n```\n%s\n```\n", in) +} + +func JSONBlock(ref interface{}) string { + return fmt.Sprintf("\n```json\n%s\n```\n", JSON(ref)) +} diff --git a/server/public/pluginapi/experimental/common/slack_attachments.go b/server/public/pluginapi/experimental/common/slack_attachments.go new file mode 100644 index 00000000000..52853c62161 --- /dev/null +++ b/server/public/pluginapi/experimental/common/slack_attachments.go @@ -0,0 +1,24 @@ +package common + +import ( + "encoding/json" + "net/http" + + "github.com/mattermost/mattermost/server/public/model" +) + +func SlackAttachmentError(w http.ResponseWriter, err error) { + response := model.PostActionIntegrationResponse{ + EphemeralText: "Error:" + err.Error(), + } + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(response) +} + +func DialogError(w http.ResponseWriter, err error) { + response := model.SubmitDialogResponse{ + Error: err.Error(), + } + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(response) +} diff --git a/server/public/pluginapi/experimental/common/url.go b/server/public/pluginapi/experimental/common/url.go new file mode 100644 index 00000000000..2faa978a836 --- /dev/null +++ b/server/public/pluginapi/experimental/common/url.go @@ -0,0 +1,28 @@ +package common + +import ( + "net/url" + "strings" + + "github.com/mattermost/mattermost/server/public/pluginapi" +) + +// GetPluginURL returns a url like siteURL/plugins/pluginID based on the information from the client. +// If any error happens in the process, a empty string is returned. +func GetPluginURL(client *pluginapi.Client) string { + mattermostSiteURL := client.Configuration.GetConfig().ServiceSettings.SiteURL + if mattermostSiteURL == nil { + return "" + } + _, err := url.Parse(*mattermostSiteURL) + if err != nil { + return "" + } + manifest, err := client.System.GetManifest() + if err != nil { + return "" + } + + pluginURLPath := "/plugins/" + manifest.Id + return strings.TrimRight(*mattermostSiteURL, "/") + pluginURLPath +} diff --git a/server/public/pluginapi/experimental/flow/flow.go b/server/public/pluginapi/experimental/flow/flow.go new file mode 100644 index 00000000000..036b1e60991 --- /dev/null +++ b/server/public/pluginapi/experimental/flow/flow.go @@ -0,0 +1,254 @@ +package flow + +import ( + "encoding/json" + "fmt" + "net/http" + "net/url" + "strings" + + "github.com/gorilla/mux" + "github.com/pkg/errors" + + "github.com/mattermost/mattermost/server/public/model" + "github.com/mattermost/mattermost/server/public/pluginapi" +) + +type Name string + +const ( + contextStepKey = "step" + contextButtonKey = "button" +) + +type Flow struct { + UserID string + state *flowState + + name Name + api *pluginapi.Client + pluginURL string + botUserID string + + steps map[Name]Step + index []Name + done func(userID string, state State) error + + debugLogState bool +} + +// NewFlow creates a new flow using direct messages with the user. +// +// name must be a unique identifier for the flow within the plugin. +func NewFlow(name Name, api *pluginapi.Client, pluginURL, botUserID string) *Flow { + return &Flow{ + name: name, + api: api, + pluginURL: pluginURL, + botUserID: botUserID, + steps: map[Name]Step{}, + } +} + +func (f *Flow) WithSteps(orderedSteps ...Step) *Flow { + if f.steps == nil { + f.steps = map[Name]Step{} + } + for _, step := range orderedSteps { + stepName := step.name + if _, ok := f.steps[stepName]; ok { + f.api.Log.Warn("ignored duplicate step name", "name", stepName, "flow", f.name) + continue + } + f.steps[stepName] = step + f.index = append(f.index, stepName) + } + return f +} + +func (f *Flow) OnDone(done func(string, State) error) *Flow { + f.done = done + return f +} + +func (f *Flow) InitHTTP(r *mux.Router) *Flow { + flowRouter := r.PathPrefix("/").Subrouter() + flowRouter.HandleFunc(namePath(f.name)+"/button", f.handleButtonHTTP).Methods(http.MethodPost) + flowRouter.HandleFunc(namePath(f.name)+"/dialog", f.handleDialogHTTP).Methods(http.MethodPost) + return f +} + +func (f *Flow) WithDebugLog() *Flow { + f.debugLogState = true + return f +} + +// ForUser creates a new flow using direct messages with the user. +func (f *Flow) ForUser(userID string) *Flow { + clone := *f + clone.UserID = userID + clone.state = nil + return &clone +} + +func (f *Flow) GetCurrentStep() (Name, error) { + state, err := f.getState() + if err != nil { + // Don't return an error if no flow is running + if errors.Is(err, errStateNotFound) { + return "", nil + } + + return "", err + } + + return state.StepName, err +} + +func (f *Flow) GetState() State { + state, _ := f.getState() + return state.AppState +} + +func (f *Flow) Start(appState State) error { + if len(f.index) == 0 { + return errors.New("no steps") + } + + err := f.storeState(flowState{ + AppState: appState, + }) + if err != nil { + return err + } + + return f.Go(f.index[0]) +} + +func (f *Flow) Finish() error { + state, err := f.getState() + if err != nil { + return err + } + + _ = f.removeState() + + if f.done != nil { + err = f.done(f.UserID, state.AppState) + } + return err +} + +func (f *Flow) Go(toName Name) error { + state, err := f.getState() + if err != nil { + return err + } + if toName == state.StepName { + // Stay at the current step, nothing to do + return nil + } + // Moving onto a different step, mark the current step as "Done" + if state.StepName != "" && !state.Done { + from, ok := f.steps[state.StepName] + if !ok { + return errors.Errorf("%s: step not found", toName) + } + + var donePost *model.Post + donePost, err = from.done(f, 0) + if err != nil { + return err + } + if donePost != nil { + donePost.Id = state.PostID + err = f.api.Post.UpdatePost(donePost) + if err != nil { + return err + } + } + } + + if toName == "" { + return f.Finish() + } + to, ok := f.steps[toName] + if !ok { + return errors.Errorf("%s: step not found", toName) + } + + post, terminal, err := to.do(f) + if err != nil { + return err + } + f.processButtonPostActions(post) + + if f.debugLogState { + data, _ := json.MarshalIndent(state, "", " ") + post.Message = fmt.Sprintf("State:\n```\n%s\n```\n", string(data)) + } + + err = f.api.Post.DM(f.botUserID, f.UserID, post) + if err != nil { + return err + } + if terminal { + return f.Finish() + } + + state.StepName = toName + state.Done = false + state.PostID = post.Id + err = f.storeState(state) + if err != nil { + return err + } + + if to.autoForward { + var nextName Name + + if to.forwardTo != "" { + nextName = to.forwardTo + } else { + nextName = f.next(toName) + } + + if nextName != "" { + return f.Go(nextName) + } + } + + return nil +} + +func (f Flow) next(fromName Name) Name { + for i, n := range f.index { + if fromName == n { + if i+1 < len(f.index) { + return f.index[i+1] + } + return "" + } + } + return "" +} + +func namePath(name Name) string { + return "/" + url.PathEscape(strings.Trim(string(name), "/")) +} + +func Goto(toName Name) func(*Flow) (Name, State, error) { + return func(_ *Flow) (Name, State, error) { + return toName, nil, nil + } +} + +func DialogGoto(toName Name) func(*Flow, map[string]interface{}) (Name, State, map[string]string, error) { + return func(_ *Flow, submitted map[string]interface{}) (Name, State, map[string]string, error) { + stateUpdate := State{} + for k, v := range submitted { + stateUpdate[k] = fmt.Sprintf("%v", v) + } + return toName, stateUpdate, nil, nil + } +} diff --git a/server/public/pluginapi/experimental/flow/handler.go b/server/public/pluginapi/experimental/flow/handler.go new file mode 100644 index 00000000000..5d5e4dac012 --- /dev/null +++ b/server/public/pluginapi/experimental/flow/handler.go @@ -0,0 +1,210 @@ +// Copyright (c) 2019-present Mattermost, Inc. All Rights Reserved. +// See License for license information. + +package flow + +import ( + "encoding/json" + "fmt" + "net/http" + + "github.com/pkg/errors" + + "github.com/mattermost/mattermost/server/public/model" + "github.com/mattermost/mattermost/server/public/pluginapi/experimental/common" +) + +func (f *Flow) handleButtonHTTP(w http.ResponseWriter, r *http.Request) { + userID := r.Header.Get("Mattermost-User-ID") + if userID == "" { + common.SlackAttachmentError(w, errors.New("Not authorized")) + return + } + f = f.ForUser(userID) + + var request model.PostActionIntegrationRequest + if err := json.NewDecoder(r.Body).Decode(&request); err != nil { + common.SlackAttachmentError(w, errors.New("invalid request")) + return + } + + // selectedButton is 1-based + fromName, selectedButton, err := buttonContext(&request) + if err != nil { + common.SlackAttachmentError(w, err) + return + } + + donePost, err := f.handleButton(fromName, selectedButton, request.TriggerId) + if err != nil { + common.SlackAttachmentError(w, err) + return + } + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(model.PostActionIntegrationResponse{ + Update: donePost, + }) +} + +func (f *Flow) handleDialogHTTP(w http.ResponseWriter, r *http.Request) { + userID := r.Header.Get("Mattermost-User-ID") + if userID == "" { + common.DialogError(w, errors.New("not authorized")) + return + } + f = f.ForUser(userID) + + var request model.SubmitDialogRequest + if err := json.NewDecoder(r.Body).Decode(&request); err != nil { + common.DialogError(w, errors.New("invalid request")) + return + } + fromName, selectedButton, err := dialogContext(&request) + if err != nil { + common.DialogError(w, errors.Wrap(err, "invalid request")) + return + } + + // handleDialog updates the post + donePost, fieldErrors, err := f.handleDialog(fromName, selectedButton, request.Submission) + if err != nil || len(fieldErrors) != 0 { + w.Header().Set("Content-Type", "application/json") + + resp := model.SubmitDialogResponse{ + Errors: fieldErrors, + } + + if err != nil { + resp.Error = err.Error() + } + + _ = json.NewEncoder(w).Encode(resp) + return + } + err = f.api.Post.UpdatePost(donePost) + if err != nil { + common.DialogError(w, err) + return + } + + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(model.SubmitDialogResponse{}) +} + +func (f *Flow) handleButton(fromName Name, selectedButton int, triggerID string) (*model.Post, error) { + post, _, err := f.handle(fromName, selectedButton, nil, triggerID, true) + return post, err +} + +func (f *Flow) handleDialog( + fromName Name, selectedButton int, submission map[string]interface{}, +) ( + *model.Post, map[string]string, error, +) { + return f.handle(fromName, selectedButton, submission, "", false) +} + +func (f *Flow) handle( + fromName Name, selectedButton int, submission map[string]interface{}, triggerID string, asButton bool, +) ( + *model.Post, map[string]string, error, +) { + state, err := f.getState() + if err != nil { + return nil, nil, err + } + if state.StepName != fromName { + return nil, nil, errors.Errorf("click from an inactive step: %v", fromName) + } + from, ok := f.steps[fromName] + if !ok { + return nil, nil, errors.Errorf("step %q not found", fromName) + } + + if selectedButton == 0 || selectedButton > len(from.buttons) { + return nil, nil, errors.Errorf("button number %v to high or too low, only %v buttons", selectedButton, len(from.buttons)) + } + b := from.buttons[selectedButton-1] + + var updated State + toName := fromName + var fieldErrors map[string]string + if asButton { + if b.OnClick != nil { + toName, updated, err = b.OnClick(f) + } + } else { + if b.OnDialogSubmit != nil { + toName, updated, fieldErrors, err = b.OnDialogSubmit(f, submission) + } + } + if err != nil || len(fieldErrors) > 0 { + return nil, fieldErrors, err + } + state.AppState = state.AppState.MergeWith(updated) + state.Done = true + err = f.storeState(state) + if err != nil { + return nil, nil, err + } + + // Empty next step name in the response indicates advancing to the next step + // in the flow. To stay on the same step the handlers should return the step + // name. + if toName == "" { + toName = f.next(fromName) + } + + if asButton && b.Dialog != nil { + if b.OnDialogSubmit == nil { + return nil, nil, errors.Errorf("no submit function for dialog, step: %s", fromName) + } + + dialogRequest := model.OpenDialogRequest{ + TriggerId: triggerID, + URL: f.pluginURL + namePath(f.name) + "/dialog", + Dialog: processDialog(b.Dialog, state.AppState), + } + dialogRequest.Dialog.State = fmt.Sprintf("%v,%v", fromName, selectedButton) + + err = f.api.Frontend.OpenInteractiveDialog(dialogRequest) + if err != nil { + return nil, nil, err + } + } + + if toName == fromName { + // Nothing else to do + return nil, nil, nil + } + + donePost, err := from.done(f, selectedButton) + if err != nil { + return nil, nil, err + } + donePost.Id = state.PostID + f.processButtonPostActions(donePost) + + err = f.Go(toName) + if err != nil { + f.api.Log.Warn("failed to advance flow to next step", "flow_name", f.name, "from", fromName, "to", toName, "error", err.Error()) + } + + // return the "done" post for the from step - leave updating up to the + // API-specific caller. + return donePost, nil, nil +} + +func (f *Flow) processButtonPostActions(post *model.Post) { + attachments, ok := post.GetProp("attachments").([]*model.SlackAttachment) + if !ok || len(attachments) == 0 { + return + } + sa := attachments[0] + for _, a := range sa.Actions { + if a.Integration == nil { + a.Integration = &model.PostActionIntegration{} + } + a.Integration.URL = f.pluginURL + namePath(f.name) + "/button" + } +} diff --git a/server/public/pluginapi/experimental/flow/state.go b/server/public/pluginapi/experimental/flow/state.go new file mode 100644 index 00000000000..e9393dc3c8d --- /dev/null +++ b/server/public/pluginapi/experimental/flow/state.go @@ -0,0 +1,146 @@ +package flow + +import ( + "bytes" + "errors" + "text/template" +) + +var errStateNotFound = errors.New("flow state not found") + +// State is the "app"'s state +type State map[string]interface{} + +func (s State) MergeWith(update State) State { + n := State{} + for k, v := range s { + n[k] = v + } + for k, v := range update { + n[k] = v + } + return n +} + +// GetString return the value to a given key as a string. +// If the key is not found or isn't a string, an empty string is returned. +func (s State) GetString(key string) string { + vRaw, ok := s[key] + if ok { + v, ok := vRaw.(string) + if ok { + return v + } + } + + return "" +} + +// GetInt return the value to a given key as a int. +// If the key is not found or isn't an int, zero is returned. +func (s State) GetInt(key string) int { + vRaw, ok := s[key] + if ok { + v, ok := vRaw.(int) + if ok { + return v + } + } + + return 0 +} + +// GetBool return the value to a given key as a bool. +// If the key is not found or isn't a bool, false is returned. +func (s State) GetBool(key string) bool { + vRaw, ok := s[key] + if ok { + v, ok := vRaw.(bool) + if ok { + return v + } + } + + return false +} + +// JSON-serializable flow state. +type flowState struct { + // The name of the step. + StepName Name + + Done bool + + // ID of the post produced by the step. + PostID string + + // Application-level state. + AppState State +} + +func (f *Flow) storeState(state flowState) error { + if f.UserID == "" { + return errors.New("no user specified") + } + + // Set AppState to differentiate an existing flow + if state.AppState == nil { + state.AppState = State{} + } + + ok, err := f.api.KV.Set(kvKey(f.UserID, f.name), state) + if err != nil { + return err + } + if !ok { + return errors.New("value not set without errors") + } + + f.state = &state + return nil +} + +func (f *Flow) getState() (flowState, error) { + if f.UserID == "" { + return flowState{}, errors.New("no user specified") + } + if f.state != nil { + return *f.state, nil + } + state := flowState{} + err := f.api.KV.Get(kvKey(f.UserID, f.name), &state) + if err != nil { + return flowState{}, err + } + if state.AppState == nil { + return flowState{}, errStateNotFound + } + + f.state = &state + return state, err +} + +func (f *Flow) removeState() error { + if f.UserID == "" { + return errors.New("no user specified") + } + f.state = nil + return f.api.KV.Delete(kvKey(f.UserID, f.name)) +} + +func kvKey(userID string, flowName Name) string { + return "_flow-" + userID + "-" + string(flowName) +} + +func formatState(source string, state State) string { + t, err := template.New("message").Parse(source) + if err != nil { + return source + " ###ERROR: " + err.Error() + } + buf := bytes.NewBuffer(nil) + err = t.Execute(buf, state) + if err != nil { + return source + " ###ERROR: " + err.Error() + } + return buf.String() +} diff --git a/server/public/pluginapi/experimental/flow/step.go b/server/public/pluginapi/experimental/flow/step.go new file mode 100644 index 00000000000..c4b5e829c8f --- /dev/null +++ b/server/public/pluginapi/experimental/flow/step.go @@ -0,0 +1,269 @@ +package flow + +import ( + "fmt" + "net/url" + "strconv" + "strings" + + "github.com/pkg/errors" + + "github.com/mattermost/mattermost/server/public/model" +) + +type Color string + +const ( + ColorDefault Color = "default" + ColorPrimary Color = "primary" + ColorSuccess Color = "success" + ColorGood Color = "good" + ColorWarning Color = "warning" + ColorDanger Color = "danger" +) + +type Step struct { + name Name + template *model.SlackAttachment + forwardTo Name + autoForward bool + terminal bool + onRender func(f *Flow) + buttons []Button +} + +type Button struct { + Name string + Disabled bool + Color Color + + // OnClick is called when the button is clicked. It returns the next step's + // name and the state updates to apply. + // + // If Dialog is also specified, OnClick is executed first. + OnClick func(f *Flow) (Name, State, error) + + // Dialog is the interactive dialog to display if the button is clicked + // (OnClick is executed first). OnDialogSubmit must be provided. + Dialog *model.Dialog + + // Function that is called when the dialog box is submitted. It can return a + // general error, or field-specific errors. On success it returns the name + // of the next step, and the state updates to apply. + OnDialogSubmit func(f *Flow, submitted map[string]interface{}) (Name, State, map[string]string, error) +} + +func NewStep(name Name) Step { + return Step{ + name: name, + template: &model.SlackAttachment{}, + } +} + +func (s Step) WithButton(buttons ...Button) Step { + s.buttons = append(s.buttons, buttons...) + return s +} + +func (s Step) Terminal() Step { + s.terminal = true + return s +} + +func (s Step) OnRender(f func(*Flow)) Step { + s.onRender = f + return s +} + +func (s Step) Next(name Name) Step { + s.forwardTo = name + s.autoForward = true + return s +} + +func (s Step) WithImage(imageURL string) Step { + if u, err := url.Parse(imageURL); err == nil { + if u.Host != "" && (u.Scheme == "http" || u.Scheme == "https") { + s.template.ImageURL = imageURL + } else { + s.template.ImageURL = u.Path + } + } + return s +} + +func (s Step) WithColor(color Color) Step { + s.template.Color = string(color) + return s +} + +func (s Step) WithPretext(text string) Step { + s.template.Pretext = text + return s +} + +func (s Step) WithField(title, value string) Step { + s.template.Fields = append(s.template.Fields, &model.SlackAttachmentField{ + Title: title, + Value: value, + }) + return s +} + +func (s Step) WithTitle(text string) Step { + s.template.Title = text + return s +} + +func (s Step) WithText(text string) Step { + s.template.Text = text + return s +} + +func (s Step) do(f *Flow) (*model.Post, bool, error) { + if s.onRender != nil { + s.onRender(f) + } + + return s.render(f, false, 0) +} + +func (s Step) done(f *Flow, selectedButton int) (*model.Post, error) { + post, _, err := s.render(f, true, selectedButton) + return post, err +} + +func (s Step) render(f *Flow, done bool, selectedButton int) (*model.Post, bool, error) { + sa := f.processAttachment(s.template) + post := model.Post{} + model.ParseSlackAttachment(&post, []*model.SlackAttachment{sa}) + + if s.terminal { + // Nothing else to do, do not display buttons on terminal posts. + return &post, true, nil + } + + buttons := processButtons(s.buttons, f.state.AppState) + + attachments, ok := post.GetProp("attachments").([]*model.SlackAttachment) + if !ok || len(attachments) != 1 { + return nil, false, errors.New("expected 1 slack attachment") + } + var actions []*model.PostAction + if done { + if selectedButton > 0 { + action := renderButton(buttons[selectedButton-1], s.name, selectedButton, f.state.AppState) + action.Disabled = true + actions = append(actions, action) + } + } else { + for i, b := range buttons { + actions = append(actions, renderButton(b, s.name, i+1, f.state.AppState)) + } + } + attachments[0].Actions = actions + return &post, false, nil +} + +func (f *Flow) processAttachment(attachment *model.SlackAttachment) *model.SlackAttachment { + if attachment == nil { + return &model.SlackAttachment{Text: "ERROR"} + } + a := *attachment + a.Pretext = formatState(attachment.Pretext, f.state.AppState) + a.Title = formatState(attachment.Title, f.state.AppState) + a.Text = formatState(attachment.Text, f.state.AppState) + + for _, field := range a.Fields { + field.Title = formatState(field.Title, f.state.AppState) + v := field.Value.(string) + if v != "" { + field.Value = formatState(v, f.state.AppState) + } + } + + a.Fallback = fmt.Sprintf("%s: %s", a.Title, a.Text) + + if attachment.ImageURL != "" { + if u, err := url.Parse(attachment.ImageURL); err == nil { + if u.Host != "" && (u.Scheme == "http" || u.Scheme == "https") { + a.ImageURL = attachment.ImageURL + } else { + a.ImageURL = f.pluginURL + "/" + strings.TrimPrefix(attachment.ImageURL, "/") + } + } + } + + return &a +} + +func processButtons(in []Button, state State) []Button { + var out []Button + for _, b := range in { + button := b + button.Name = formatState(b.Name, state) + out = append(out, button) + } + return out +} + +func processDialog(in *model.Dialog, state State) model.Dialog { + d := *in + d.Title = formatState(d.Title, state) + d.IntroductionText = formatState(d.IntroductionText, state) + d.SubmitLabel = formatState(d.SubmitLabel, state) + for i := range d.Elements { + d.Elements[i].DisplayName = formatState(d.Elements[i].DisplayName, state) + d.Elements[i].Name = formatState(d.Elements[i].Name, state) + d.Elements[i].Default = formatState(d.Elements[i].Default, state) + d.Elements[i].Placeholder = formatState(d.Elements[i].Placeholder, state) + d.Elements[i].HelpText = formatState(d.Elements[i].HelpText, state) + } + return d +} + +func renderButton(b Button, stepName Name, i int, state State) *model.PostAction { + return &model.PostAction{ + Name: formatState(b.Name, state), + Disabled: b.Disabled, + Style: string(b.Color), + Integration: &model.PostActionIntegration{ + Context: map[string]interface{}{ + contextStepKey: string(stepName), + contextButtonKey: strconv.Itoa(i), + }, + }, + } +} + +func buttonContext(request *model.PostActionIntegrationRequest) (Name, int, error) { + fromString, ok := request.Context[contextStepKey].(string) + if !ok { + return "", 0, errors.New("missing step name") + } + fromName := Name(fromString) + + buttonStr, ok := request.Context[contextButtonKey].(string) + if !ok { + return "", 0, errors.New("missing button id") + } + buttonIndex, err := strconv.Atoi(buttonStr) + if err != nil { + return "", 0, errors.Wrap(err, "invalid button number") + } + + return fromName, buttonIndex, nil +} + +func dialogContext(request *model.SubmitDialogRequest) (Name, int, error) { + data := strings.Split(request.State, ",") + if len(data) != 2 { + return "", 0, errors.New("invalid request") + } + fromName := Name(data[0]) + buttonIndex, err := strconv.Atoi(data[1]) + if err != nil { + return "", 0, errors.Wrap(err, "malformed button number") + } + return fromName, buttonIndex, nil +} diff --git a/server/public/pluginapi/experimental/oauther/mock_oauther/mock_oauther.go b/server/public/pluginapi/experimental/oauther/mock_oauther/mock_oauther.go new file mode 100644 index 00000000000..22c1f8d5944 --- /dev/null +++ b/server/public/pluginapi/experimental/oauther/mock_oauther/mock_oauther.go @@ -0,0 +1,77 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/mattermost/mattermost-plugin-mscalendar/server/utils/oauther (interfaces: OAuther) + +// Package mock_oauther is a generated GoMock package. +package mock_oauther + +import ( + gomock "github.com/golang/mock/gomock" + oauth2 "golang.org/x/oauth2" + reflect "reflect" +) + +// MockOAuther is a mock of OAuther interface +type MockOAuther struct { + ctrl *gomock.Controller + recorder *MockOAutherMockRecorder +} + +// MockOAutherMockRecorder is the mock recorder for MockOAuther +type MockOAutherMockRecorder struct { + mock *MockOAuther +} + +// NewMockOAuther creates a new mock instance +func NewMockOAuther(ctrl *gomock.Controller) *MockOAuther { + mock := &MockOAuther{ctrl: ctrl} + mock.recorder = &MockOAutherMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use +func (m *MockOAuther) EXPECT() *MockOAutherMockRecorder { + return m.recorder +} + +// Deauth mocks base method +func (m *MockOAuther) Deauth(arg0 string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Deauth", arg0) + ret0, _ := ret[0].(error) + return ret0 +} + +// Deauth indicates an expected call of Deauth +func (mr *MockOAutherMockRecorder) Deauth(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Deauth", reflect.TypeOf((*MockOAuther)(nil).Deauth), arg0) +} + +// GetToken mocks base method +func (m *MockOAuther) GetToken(arg0 string) (*oauth2.Token, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetToken", arg0) + ret0, _ := ret[0].(*oauth2.Token) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetToken indicates an expected call of GetToken +func (mr *MockOAutherMockRecorder) GetToken(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetToken", reflect.TypeOf((*MockOAuther)(nil).GetToken), arg0) +} + +// GetURL mocks base method +func (m *MockOAuther) GetURL() string { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetURL") + ret0, _ := ret[0].(string) + return ret0 +} + +// GetURL indicates an expected call of GetURL +func (mr *MockOAutherMockRecorder) GetURL() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetURL", reflect.TypeOf((*MockOAuther)(nil).GetURL)) +} diff --git a/server/public/pluginapi/experimental/oauther/mocks/mock_oauther.go b/server/public/pluginapi/experimental/oauther/mocks/mock_oauther.go new file mode 100644 index 00000000000..83e6cc7256e --- /dev/null +++ b/server/public/pluginapi/experimental/oauther/mocks/mock_oauther.go @@ -0,0 +1,105 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/mattermost/mattermost/server/public/pluginapi/experimental/oauther (interfaces: OAuther) + +// Package mock_oauther is a generated GoMock package. +package mock_oauther + +import ( + http "net/http" + reflect "reflect" + + gomock "github.com/golang/mock/gomock" + oauth2 "golang.org/x/oauth2" +) + +// MockOAuther is a mock of OAuther interface. +type MockOAuther struct { + ctrl *gomock.Controller + recorder *MockOAutherMockRecorder +} + +// MockOAutherMockRecorder is the mock recorder for MockOAuther. +type MockOAutherMockRecorder struct { + mock *MockOAuther +} + +// NewMockOAuther creates a new mock instance. +func NewMockOAuther(ctrl *gomock.Controller) *MockOAuther { + mock := &MockOAuther{ctrl: ctrl} + mock.recorder = &MockOAutherMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockOAuther) EXPECT() *MockOAutherMockRecorder { + return m.recorder +} + +// AddPayload mocks base method. +func (m *MockOAuther) AddPayload(arg0 string, arg1 []byte) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "AddPayload", arg0, arg1) + ret0, _ := ret[0].(error) + return ret0 +} + +// AddPayload indicates an expected call of AddPayload. +func (mr *MockOAutherMockRecorder) AddPayload(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddPayload", reflect.TypeOf((*MockOAuther)(nil).AddPayload), arg0, arg1) +} + +// Deauthorize mocks base method. +func (m *MockOAuther) Deauthorize(arg0 string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Deauthorize", arg0) + ret0, _ := ret[0].(error) + return ret0 +} + +// Deauthorize indicates an expected call of Deauthorize. +func (mr *MockOAutherMockRecorder) Deauthorize(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Deauthorize", reflect.TypeOf((*MockOAuther)(nil).Deauthorize), arg0) +} + +// GetConnectURL mocks base method. +func (m *MockOAuther) GetConnectURL() string { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetConnectURL") + ret0, _ := ret[0].(string) + return ret0 +} + +// GetConnectURL indicates an expected call of GetConnectURL. +func (mr *MockOAutherMockRecorder) GetConnectURL() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetConnectURL", reflect.TypeOf((*MockOAuther)(nil).GetConnectURL)) +} + +// GetToken mocks base method. +func (m *MockOAuther) GetToken(arg0 string) (*oauth2.Token, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetToken", arg0) + ret0, _ := ret[0].(*oauth2.Token) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetToken indicates an expected call of GetToken. +func (mr *MockOAutherMockRecorder) GetToken(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetToken", reflect.TypeOf((*MockOAuther)(nil).GetToken), arg0) +} + +// ServeHTTP mocks base method. +func (m *MockOAuther) ServeHTTP(arg0 http.ResponseWriter, arg1 *http.Request) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "ServeHTTP", arg0, arg1) +} + +// ServeHTTP indicates an expected call of ServeHTTP. +func (mr *MockOAutherMockRecorder) ServeHTTP(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ServeHTTP", reflect.TypeOf((*MockOAuther)(nil).ServeHTTP), arg0, arg1) +} diff --git a/server/public/pluginapi/experimental/oauther/oauth2.go b/server/public/pluginapi/experimental/oauther/oauth2.go new file mode 100644 index 00000000000..ccbaea2c133 --- /dev/null +++ b/server/public/pluginapi/experimental/oauther/oauth2.go @@ -0,0 +1,191 @@ +// Copyright (c) 2019-present Mattermost, Inc. All Rights Reserved. +// See License for license information. + +package oauther + +import ( + "net/http" + "time" + + "golang.org/x/oauth2" + + "github.com/mattermost/mattermost/server/public/pluginapi" + "github.com/mattermost/mattermost/server/public/pluginapi/experimental/bot/logger" + "github.com/mattermost/mattermost/server/public/pluginapi/experimental/common" +) + +const ( + // DefaultStorePrefix is the prefix used when storing information in the KVStore by default. + DefaultStorePrefix = "oauth_" + // DefaultOAuthURL is the URL the OAuther will use to register its endpoints by default. + DefaultOAuthURL = "/oauth2" + // DefaultConnectedString is the string shown to the user when the oauth flow is completed by default. + DefaultConnectedString = "Successfully connected. Please close this window." + // DefaultOAuth2StateTimeToLive is the duration the states from the OAuth flow will live in the KVStore by default. + DefaultOAuth2StateTimeToLive = 5 * time.Minute + // DefaultPayloadTimeToLive is the duration the user payload will live in the KVStore by default. + DefaultPayloadTimeToLive = 10 * time.Minute +) + +const ( + connectURL = "/connect" + completeURL = "/complete" +) + +// OAuther defines an object able to perform the OAuth flow. +type OAuther interface { + // GetToken returns the oauth token for userID, or error if it does not exist or there is any store error. + GetToken(userID string) (*oauth2.Token, error) + // GetConnectURL returns the URL to reach in order to start the OAuth flow. + GetConnectURL() string + // Deauthorize removes the token for userID. Return error if there is any store error. + Deauthorize(userID string) error + // ServeHTTP implements http.Handler + ServeHTTP(w http.ResponseWriter, r *http.Request) + // AddPayload stores some information to be returned after the flow is over + AddPayload(userID string, payload []byte) error +} + +type oAuther struct { + pluginURL string + config oauth2.Config + onConnect func(userID string, token oauth2.Token, payload []byte) + store common.KVStore + logger logger.Logger + storePrefix string + oAuthURL string + connectedString string + oAuth2StateTimeToLive time.Duration + payloadTimeToLive time.Duration +} + +/* +New creates a new OAuther. + +- pluginURL: The base URL for the plugin (e.g. https://www.instance.com/plugins/pluginid). + +- oAuthConfig: The configuration of the Authorization flow to perform. + +- onConnect: What to do when the Authorization process is complete. + +- store: A KVStore to store the data of the OAuther. + +- l Logger: A logger to log errors during authorization. + +- options: Optional options for the OAuther. Available options are StorePrefix, OAuthURL, ConnectedString and OAuth2StateTimeToLive. +*/ +func New( + pluginURL string, + oAuthConfig oauth2.Config, + onConnect func(userID string, token oauth2.Token, payload []byte), + store common.KVStore, + l logger.Logger, + options ...Option, +) OAuther { + o := &oAuther{ + pluginURL: pluginURL, + config: oAuthConfig, + onConnect: onConnect, + store: store, + logger: l, + storePrefix: DefaultStorePrefix, + oAuthURL: DefaultOAuthURL, + connectedString: DefaultConnectedString, + oAuth2StateTimeToLive: DefaultOAuth2StateTimeToLive, + payloadTimeToLive: DefaultPayloadTimeToLive, + } + + for _, option := range options { + option(o) + } + + o.config.RedirectURL = o.pluginURL + o.oAuthURL + "/complete" + + return o +} + +/* +NewFromClient creates a new OAuther from the plugin api client. + +- pluginapi: A plugin api client. + +- pluginID: The plugin ID. + +- oAuthConfig: The configuration of the Authorization flow to perform. + +- onConnect: What to do when the Authorization process is complete. + +- l Logger: A logger to log errors during authorization. + +- options: Optional options for the OAuther. Available options are StorePrefix, OAuthURL, ConnectedString and OAuth2StateTimeToLive. +*/ +func NewFromClient( + client *pluginapi.Client, + oAuthConfig oauth2.Config, + onConnect func(userID string, token oauth2.Token, payload []byte), + l logger.Logger, + options ...Option, +) OAuther { + return New( + common.GetPluginURL(client), + oAuthConfig, + onConnect, + &client.KV, + l, + options..., + ) +} + +func (o *oAuther) GetConnectURL() string { + return o.pluginURL + o.oAuthURL + "/connect" +} + +func (o *oAuther) GetToken(userID string) (*oauth2.Token, error) { + var token *oauth2.Token + err := o.store.Get(o.getTokenKey(userID), &token) + if err != nil { + return nil, err + } + return token, nil +} + +func (o *oAuther) getTokenKey(userID string) string { + return o.storePrefix + "token_" + userID +} + +func (o *oAuther) getStateKey(userID string) string { + return o.storePrefix + "state_" + userID +} + +func (o *oAuther) getPayloadKey(userID string) string { + return o.storePrefix + "payload_" + userID +} + +func (o *oAuther) Deauthorize(userID string) error { + err := o.store.Delete(o.getTokenKey(userID)) + if err != nil { + return err + } + + return nil +} + +func (o *oAuther) ServeHTTP(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case o.oAuthURL + connectURL: + o.oauth2Connect(w, r) + case o.oAuthURL + completeURL: + o.oauth2Complete(w, r) + default: + http.NotFound(w, r) + } +} + +func (o *oAuther) AddPayload(userID string, payload []byte) error { + _, err := o.store.Set(o.getPayloadKey(userID), payload, pluginapi.SetExpiry(o.payloadTimeToLive)) + if err != nil { + return err + } + + return nil +} diff --git a/server/public/pluginapi/experimental/oauther/oauth2_complete.go b/server/public/pluginapi/experimental/oauther/oauth2_complete.go new file mode 100644 index 00000000000..486355c9aa8 --- /dev/null +++ b/server/public/pluginapi/experimental/oauther/oauth2_complete.go @@ -0,0 +1,105 @@ +// Copyright (c) 2019-present Mattermost, Inc. All Rights Reserved. +// See License for license information. + +package oauther + +import ( + "context" + "fmt" + "net/http" + "strings" +) + +func (o *oAuther) oauth2Complete(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet { + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + return + } + authedUserID := r.Header.Get("Mattermost-User-ID") + if authedUserID == "" { + o.logger.Debugf("oauth2Complete: reached by non authed user") + http.Error(w, "Not authorized", http.StatusUnauthorized) + return + } + code := r.URL.Query().Get("code") + if code == "" { + o.logger.Debugf("oauth2Complete: reached with no code") + http.Error(w, "Bad request", http.StatusBadRequest) + return + } + state := r.URL.Query().Get("state") + + var storedState string + err := o.store.Get(o.getStateKey(authedUserID), &storedState) + if err != nil { + o.logger.Warnf("oauth2Complete: cannot get state, err=%s", err.Error()) + http.Error(w, "Internal server error", http.StatusInternalServerError) + return + } + + if storedState != state { + o.logger.Debugf("oauth2Complete: state mismatch") + o.logger.Debugf("received state '%s'; expected state '%s%", state, storedState) + http.Error(w, "Not authorized", http.StatusUnauthorized) + return + } + + userID := strings.Split(state, "_")[1] + if userID != authedUserID { + o.logger.Debugf("oauth2Complete: authed user mismatch") + http.Error(w, "Not authorized", http.StatusUnauthorized) + return + } + + ctx := context.Background() + token, err := o.config.Exchange(ctx, code) + if err != nil { + o.logger.Warnf("oauth2Complete: could not generate token, err=%s", err.Error()) + http.Error(w, "Not authorized", http.StatusUnauthorized) + return + } + + var payload []byte + err = o.store.Get(o.getPayloadKey(userID), &payload) + if err != nil { + o.logger.Errorf("oauth2Complete: could not fetch payload, err=&s", err.Error()) + http.Error(w, "Internal server error", http.StatusInternalServerError) + return + } + + ok, err := o.store.Set(o.getTokenKey(userID), token) + if err != nil { + o.logger.Errorf("oauth2Complete: cannot store the token, err=%s", err.Error()) + http.Error(w, "Internal server error", http.StatusInternalServerError) + return + } + if !ok { + o.logger.Errorf("oauth2Complete: cannot store token without error") + http.Error(w, "Internal server error", http.StatusInternalServerError) + return + } + + html := fmt.Sprintf(` + + + + + + +

%s

+ + + `, o.connectedString) + + w.Header().Set("Content-Type", "text/html") + _, err = w.Write([]byte(html)) + if err != nil { + o.logger.Errorf("oauth2Complete: error writing response, err=%s", err.Error()) + } + + if o.onConnect != nil { + o.onConnect(userID, *token, payload) + } +} diff --git a/server/public/pluginapi/experimental/oauther/oauth2_connect.go b/server/public/pluginapi/experimental/oauther/oauth2_connect.go new file mode 100644 index 00000000000..bc4822942cc --- /dev/null +++ b/server/public/pluginapi/experimental/oauther/oauth2_connect.go @@ -0,0 +1,38 @@ +// Copyright (c) 2019-present Mattermost, Inc. All Rights Reserved. +// See License for license information. + +package oauther + +import ( + "fmt" + "net/http" + + "golang.org/x/oauth2" + + "github.com/mattermost/mattermost/server/public/model" + "github.com/mattermost/mattermost/server/public/pluginapi" +) + +func (o *oAuther) oauth2Connect(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet { + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + return + } + userID := r.Header.Get("Mattermost-User-ID") + if userID == "" { + o.logger.Debugf("oauth2Connect: reached by non authed user") + http.Error(w, "Not authorized", http.StatusUnauthorized) + return + } + + state := fmt.Sprintf("%v_%v", model.NewId()[0:15], userID) + _, err := o.store.Set(o.getStateKey(userID), state, pluginapi.SetExpiry(o.oAuth2StateTimeToLive)) + if err != nil { + o.logger.Errorf("oauth2Connect: failed to store state, err=%s", err.Error()) + http.Error(w, "failed to store token state", http.StatusInternalServerError) + return + } + + redirectURL := o.config.AuthCodeURL(state, oauth2.AccessTypeOffline) + http.Redirect(w, r, redirectURL, http.StatusFound) +} diff --git a/server/public/pluginapi/experimental/oauther/options.go b/server/public/pluginapi/experimental/oauther/options.go new file mode 100644 index 00000000000..c371218265c --- /dev/null +++ b/server/public/pluginapi/experimental/oauther/options.go @@ -0,0 +1,47 @@ +package oauther + +import "time" + +// Option defines each option that can be passed in the creation of the OAuther. +// Options functions available are OAuthURL, StorePrefix, ConnectedString and OAuth2StateTimeToLive and PayloadTimeToLive. +type Option func(*oAuther) + +// OAuthURL defines the URL the OAuther will use to register its endpoints. +// Defaults to "/oauth2". +func OAuthURL(url string) Option { + return func(o *oAuther) { + o.oAuthURL = url + } +} + +// StorePrefix defines the prefix the OAuther will use to store information in the KVStore. +// Defaults to "oauth_". +func StorePrefix(prefix string) Option { + return func(o *oAuther) { + o.storePrefix = prefix + } +} + +// ConnectedString defines the string shown to the user when the oauth flow is completed. +// Defaults to "Successfully connected. Please close this window.". +func ConnectedString(text string) Option { + return func(o *oAuther) { + o.connectedString = text + } +} + +// OAuth2StateTimeToLive is the duration the states from the OAuth flow will live in the KVStore. +// Defaults to 5 minutes. +func OAuth2StateTimeToLive(ttl time.Duration) Option { + return func(o *oAuther) { + o.oAuth2StateTimeToLive = ttl + } +} + +// PayloadTimeToLive is the duration the payload from the OAuth flow will live in the KVStore. +// Defaults to 10 minutes. +func PayloadTimeToLive(ttl time.Duration) Option { + return func(o *oAuther) { + o.payloadTimeToLive = ttl + } +} diff --git a/server/public/pluginapi/experimental/panel/handler.go b/server/public/pluginapi/experimental/panel/handler.go new file mode 100644 index 00000000000..72bbeb735b0 --- /dev/null +++ b/server/public/pluginapi/experimental/panel/handler.go @@ -0,0 +1,70 @@ +package panel + +import ( + "encoding/json" + "net/http" + + "github.com/gorilla/mux" + "github.com/pkg/errors" + + "github.com/mattermost/mattermost/server/public/model" + "github.com/mattermost/mattermost/server/public/pluginapi/experimental/common" + "github.com/mattermost/mattermost/server/public/pluginapi/experimental/panel/settings" +) + +type handler struct { + panel Panel +} + +func Init(r *mux.Router, panel Panel) { + sh := &handler{ + panel: panel, + } + + panelRouter := r.PathPrefix("/").Subrouter() + panelRouter.HandleFunc(panel.URL(), sh.handleAction).Methods(http.MethodPost) +} + +func (sh *handler) handleAction(w http.ResponseWriter, r *http.Request) { + mattermostUserID := r.Header.Get("Mattermost-User-ID") + if mattermostUserID == "" { + common.SlackAttachmentError(w, errors.New("Not authorized")) + return + } + + var request model.PostActionIntegrationRequest + if err := json.NewDecoder(r.Body).Decode(&request); err != nil { + common.SlackAttachmentError(w, errors.New("invalid request")) + return + } + + id, ok := request.Context[settings.ContextIDKey] + if !ok { + common.SlackAttachmentError(w, errors.New("missing setting id")) + return + } + + value, ok := request.Context[settings.ContextButtonValueKey] + if !ok { + value, ok = request.Context[settings.ContextOptionValueKey] + if !ok { + common.SlackAttachmentError(w, errors.New("valid key not found")) + return + } + } + + idString := id.(string) + err := sh.panel.Set(mattermostUserID, idString, value) + if err != nil { + common.SlackAttachmentError(w, errors.Wrap(err, "cannot save setting")) + return + } + + response := model.PostActionIntegrationResponse{} + post, err := sh.panel.ToPost(mattermostUserID) + if err == nil { + response.Update = post + } + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(response) +} diff --git a/server/public/pluginapi/experimental/panel/mocks/mock_panel.go b/server/public/pluginapi/experimental/panel/mocks/mock_panel.go new file mode 100644 index 00000000000..c23eb558b3d --- /dev/null +++ b/server/public/pluginapi/experimental/panel/mocks/mock_panel.go @@ -0,0 +1,118 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/mattermost/mattermost/server/public/pluginapi/experimental/panel (interfaces: Panel) + +// Package mock_panel is a generated GoMock package. +package mock_panel + +import ( + reflect "reflect" + + gomock "github.com/golang/mock/gomock" + model "github.com/mattermost/mattermost/server/public/model" +) + +// MockPanel is a mock of Panel interface. +type MockPanel struct { + ctrl *gomock.Controller + recorder *MockPanelMockRecorder +} + +// MockPanelMockRecorder is the mock recorder for MockPanel. +type MockPanelMockRecorder struct { + mock *MockPanel +} + +// NewMockPanel creates a new mock instance. +func NewMockPanel(ctrl *gomock.Controller) *MockPanel { + mock := &MockPanel{ctrl: ctrl} + mock.recorder = &MockPanelMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockPanel) EXPECT() *MockPanelMockRecorder { + return m.recorder +} + +// Clear mocks base method. +func (m *MockPanel) Clear(arg0 string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Clear", arg0) + ret0, _ := ret[0].(error) + return ret0 +} + +// Clear indicates an expected call of Clear. +func (mr *MockPanelMockRecorder) Clear(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Clear", reflect.TypeOf((*MockPanel)(nil).Clear), arg0) +} + +// GetSettingIDs mocks base method. +func (m *MockPanel) GetSettingIDs() []string { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetSettingIDs") + ret0, _ := ret[0].([]string) + return ret0 +} + +// GetSettingIDs indicates an expected call of GetSettingIDs. +func (mr *MockPanelMockRecorder) GetSettingIDs() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSettingIDs", reflect.TypeOf((*MockPanel)(nil).GetSettingIDs)) +} + +// Print mocks base method. +func (m *MockPanel) Print(arg0 string) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "Print", arg0) +} + +// Print indicates an expected call of Print. +func (mr *MockPanelMockRecorder) Print(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Print", reflect.TypeOf((*MockPanel)(nil).Print), arg0) +} + +// Set mocks base method. +func (m *MockPanel) Set(arg0, arg1 string, arg2 interface{}) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Set", arg0, arg1, arg2) + ret0, _ := ret[0].(error) + return ret0 +} + +// Set indicates an expected call of Set. +func (mr *MockPanelMockRecorder) Set(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Set", reflect.TypeOf((*MockPanel)(nil).Set), arg0, arg1, arg2) +} + +// ToPost mocks base method. +func (m *MockPanel) ToPost(arg0 string) (*model.Post, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ToPost", arg0) + ret0, _ := ret[0].(*model.Post) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ToPost indicates an expected call of ToPost. +func (mr *MockPanelMockRecorder) ToPost(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ToPost", reflect.TypeOf((*MockPanel)(nil).ToPost), arg0) +} + +// URL mocks base method. +func (m *MockPanel) URL() string { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "URL") + ret0, _ := ret[0].(string) + return ret0 +} + +// URL indicates an expected call of URL. +func (mr *MockPanelMockRecorder) URL() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "URL", reflect.TypeOf((*MockPanel)(nil).URL)) +} diff --git a/server/public/pluginapi/experimental/panel/mocks/mock_panelStore.go b/server/public/pluginapi/experimental/panel/mocks/mock_panelStore.go new file mode 100644 index 00000000000..90383b33147 --- /dev/null +++ b/server/public/pluginapi/experimental/panel/mocks/mock_panelStore.go @@ -0,0 +1,77 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/mattermost/mattermost/server/public/pluginapi/experimental/panel (interfaces: Store) + +// Package mock_panel is a generated GoMock package. +package mock_panel + +import ( + reflect "reflect" + + gomock "github.com/golang/mock/gomock" +) + +// MockStore is a mock of Store interface. +type MockStore struct { + ctrl *gomock.Controller + recorder *MockStoreMockRecorder +} + +// MockStoreMockRecorder is the mock recorder for MockStore. +type MockStoreMockRecorder struct { + mock *MockStore +} + +// NewMockStore creates a new mock instance. +func NewMockStore(ctrl *gomock.Controller) *MockStore { + mock := &MockStore{ctrl: ctrl} + mock.recorder = &MockStoreMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockStore) EXPECT() *MockStoreMockRecorder { + return m.recorder +} + +// DeletePanelPostID mocks base method. +func (m *MockStore) DeletePanelPostID(arg0 string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DeletePanelPostID", arg0) + ret0, _ := ret[0].(error) + return ret0 +} + +// DeletePanelPostID indicates an expected call of DeletePanelPostID. +func (mr *MockStoreMockRecorder) DeletePanelPostID(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeletePanelPostID", reflect.TypeOf((*MockStore)(nil).DeletePanelPostID), arg0) +} + +// GetPanelPostID mocks base method. +func (m *MockStore) GetPanelPostID(arg0 string) (string, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetPanelPostID", arg0) + ret0, _ := ret[0].(string) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetPanelPostID indicates an expected call of GetPanelPostID. +func (mr *MockStoreMockRecorder) GetPanelPostID(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPanelPostID", reflect.TypeOf((*MockStore)(nil).GetPanelPostID), arg0) +} + +// SetPanelPostID mocks base method. +func (m *MockStore) SetPanelPostID(arg0, arg1 string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SetPanelPostID", arg0, arg1) + ret0, _ := ret[0].(error) + return ret0 +} + +// SetPanelPostID indicates an expected call of SetPanelPostID. +func (mr *MockStoreMockRecorder) SetPanelPostID(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetPanelPostID", reflect.TypeOf((*MockStore)(nil).SetPanelPostID), arg0, arg1) +} diff --git a/server/public/pluginapi/experimental/panel/mocks/mock_setting.go b/server/public/pluginapi/experimental/panel/mocks/mock_setting.go new file mode 100644 index 00000000000..38b87f44ac9 --- /dev/null +++ b/server/public/pluginapi/experimental/panel/mocks/mock_setting.go @@ -0,0 +1,149 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/mattermost/mattermost/server/public/pluginapi/experimental/panel/settings (interfaces: Setting) + +// Package mock_panel is a generated GoMock package. +package mock_panel + +import ( + reflect "reflect" + + gomock "github.com/golang/mock/gomock" + model "github.com/mattermost/mattermost/server/public/model" +) + +// MockSetting is a mock of Setting interface. +type MockSetting struct { + ctrl *gomock.Controller + recorder *MockSettingMockRecorder +} + +// MockSettingMockRecorder is the mock recorder for MockSetting. +type MockSettingMockRecorder struct { + mock *MockSetting +} + +// NewMockSetting creates a new mock instance. +func NewMockSetting(ctrl *gomock.Controller) *MockSetting { + mock := &MockSetting{ctrl: ctrl} + mock.recorder = &MockSettingMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockSetting) EXPECT() *MockSettingMockRecorder { + return m.recorder +} + +// Get mocks base method. +func (m *MockSetting) Get(arg0 string) (interface{}, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Get", arg0) + ret0, _ := ret[0].(interface{}) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Get indicates an expected call of Get. +func (mr *MockSettingMockRecorder) Get(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockSetting)(nil).Get), arg0) +} + +// GetDependency mocks base method. +func (m *MockSetting) GetDependency() string { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetDependency") + ret0, _ := ret[0].(string) + return ret0 +} + +// GetDependency indicates an expected call of GetDependency. +func (mr *MockSettingMockRecorder) GetDependency() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetDependency", reflect.TypeOf((*MockSetting)(nil).GetDependency)) +} + +// GetDescription mocks base method. +func (m *MockSetting) GetDescription() string { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetDescription") + ret0, _ := ret[0].(string) + return ret0 +} + +// GetDescription indicates an expected call of GetDescription. +func (mr *MockSettingMockRecorder) GetDescription() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetDescription", reflect.TypeOf((*MockSetting)(nil).GetDescription)) +} + +// GetID mocks base method. +func (m *MockSetting) GetID() string { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetID") + ret0, _ := ret[0].(string) + return ret0 +} + +// GetID indicates an expected call of GetID. +func (mr *MockSettingMockRecorder) GetID() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetID", reflect.TypeOf((*MockSetting)(nil).GetID)) +} + +// GetSlackAttachments mocks base method. +func (m *MockSetting) GetSlackAttachments(arg0, arg1 string, arg2 bool) (*model.SlackAttachment, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetSlackAttachments", arg0, arg1, arg2) + ret0, _ := ret[0].(*model.SlackAttachment) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetSlackAttachments indicates an expected call of GetSlackAttachments. +func (mr *MockSettingMockRecorder) GetSlackAttachments(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSlackAttachments", reflect.TypeOf((*MockSetting)(nil).GetSlackAttachments), arg0, arg1, arg2) +} + +// GetTitle mocks base method. +func (m *MockSetting) GetTitle() string { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetTitle") + ret0, _ := ret[0].(string) + return ret0 +} + +// GetTitle indicates an expected call of GetTitle. +func (mr *MockSettingMockRecorder) GetTitle() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTitle", reflect.TypeOf((*MockSetting)(nil).GetTitle)) +} + +// IsDisabled mocks base method. +func (m *MockSetting) IsDisabled(arg0 interface{}) bool { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "IsDisabled", arg0) + ret0, _ := ret[0].(bool) + return ret0 +} + +// IsDisabled indicates an expected call of IsDisabled. +func (mr *MockSettingMockRecorder) IsDisabled(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsDisabled", reflect.TypeOf((*MockSetting)(nil).IsDisabled), arg0) +} + +// Set mocks base method. +func (m *MockSetting) Set(arg0 string, arg1 interface{}) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Set", arg0, arg1) + ret0, _ := ret[0].(error) + return ret0 +} + +// Set indicates an expected call of Set. +func (mr *MockSettingMockRecorder) Set(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Set", reflect.TypeOf((*MockSetting)(nil).Set), arg0, arg1) +} diff --git a/server/public/pluginapi/experimental/panel/panel.go b/server/public/pluginapi/experimental/panel/panel.go new file mode 100644 index 00000000000..e5e9582576c --- /dev/null +++ b/server/public/pluginapi/experimental/panel/panel.go @@ -0,0 +1,171 @@ +package panel + +import ( + "errors" + + "github.com/mattermost/mattermost/server/public/model" + "github.com/mattermost/mattermost/server/public/pluginapi/experimental/bot/logger" + "github.com/mattermost/mattermost/server/public/pluginapi/experimental/bot/poster" + "github.com/mattermost/mattermost/server/public/pluginapi/experimental/common" + "github.com/mattermost/mattermost/server/public/pluginapi/experimental/panel/settings" +) + +type Panel interface { + Set(userID, settingID string, value interface{}) error + Print(userID string) + ToPost(userID string) (*model.Post, error) + Clear(userID string) error + URL() string + GetSettingIDs() []string +} + +type panel struct { + settings map[string]settings.Setting + settingKeys []string + poster poster.Poster + logger logger.Logger + store Store + settingHandler string + pluginURL string +} + +func NewSettingsPanel( + settingList []settings.Setting, + p poster.Poster, + l logger.Logger, + store Store, + settingHandler, + pluginURL string, +) Panel { + settingsMap := make(map[string]settings.Setting) + settingKeys := []string{} + for _, s := range settingList { + settingsMap[s.GetID()] = s + settingKeys = append(settingKeys, s.GetID()) + } + + panel := &panel{ + settings: settingsMap, + settingKeys: settingKeys, + poster: p, + logger: l, + store: store, + settingHandler: settingHandler, + pluginURL: pluginURL, + } + + return panel +} + +func (p *panel) Set(userID, settingID string, value interface{}) error { + s, ok := p.settings[settingID] + if !ok { + return errors.New("cannot find setting " + settingID) + } + + err := s.Set(userID, value) + if err != nil { + return err + } + return nil +} + +func (p *panel) GetSettingIDs() []string { + return p.settingKeys +} + +func (p *panel) URL() string { + return p.settingHandler +} + +func (p *panel) Print(userID string) { + err := p.cleanPreviousSettingsPosts(userID) + if err != nil { + p.logger.Errorf("could not clean previous setting post, " + err.Error()) + } + + sas := []*model.SlackAttachment{} + for _, key := range p.settingKeys { + s := p.settings[key] + sa, loopErr := s.GetSlackAttachments(userID, p.pluginURL+p.settingHandler, p.isSettingDisabled(userID, s)) + if loopErr != nil { + p.logger.Errorf("error creating the slack attachment, err=" + loopErr.Error()) + continue + } + sas = append(sas, sa) + } + postID, err := p.poster.DMWithAttachments(userID, sas...) + if err != nil { + p.logger.Errorf("error creating the message, err=", err.Error()) + return + } + + err = p.store.SetPanelPostID(userID, postID) + if err != nil { + p.logger.Errorf("could not set the post IDs, err=", err.Error()) + } +} + +func (p *panel) ToPost(userID string) (*model.Post, error) { + post := &model.Post{} + + sas := []*model.SlackAttachment{} + for _, key := range p.settingKeys { + s := p.settings[key] + sa, err := s.GetSlackAttachments(userID, p.pluginURL+p.settingHandler, p.isSettingDisabled(userID, s)) + if err != nil { + p.logger.Errorf("error creating the slack attachment for setting %s, err=%s", s.GetID(), err.Error()) + continue + } + sas = append(sas, sa) + } + + model.ParseSlackAttachment(post, sas) + return post, nil +} + +func (p *panel) cleanPreviousSettingsPosts(userID string) error { + postID, err := p.store.GetPanelPostID(userID) + if err == common.ErrNotFound { + return nil + } + + if err != nil { + return err + } + + err = p.poster.DeletePost(postID) + if err != nil { + p.logger.Errorf("could not delete setting post, %s", err) + } + + err = p.store.DeletePanelPostID(userID) + if err != nil { + return err + } + + return nil +} + +func (p *panel) Clear(userID string) error { + return p.cleanPreviousSettingsPosts(userID) +} + +func (p *panel) isSettingDisabled(userID string, s settings.Setting) bool { + dependencyID := s.GetDependency() + if dependencyID == "" { + return false + } + dependency, ok := p.settings[dependencyID] + if !ok { + p.logger.Errorf("settings dependency %s not found", dependencyID) + return false + } + + value, err := dependency.Get(userID) + if err != nil { + p.logger.Errorf("cannot get dependency %s value", dependencyID) + return false + } + return s.IsDisabled(value) +} diff --git a/server/public/pluginapi/experimental/panel/settings/base_setting.go b/server/public/pluginapi/experimental/panel/settings/base_setting.go new file mode 100644 index 00000000000..96d533e578d --- /dev/null +++ b/server/public/pluginapi/experimental/panel/settings/base_setting.go @@ -0,0 +1,28 @@ +package settings + +type baseSetting struct { + title string + description string + id string + dependsOn string +} + +func (s *baseSetting) GetID() string { + return s.id +} + +func (s *baseSetting) GetTitle() string { + return s.title +} + +func (s *baseSetting) GetDescription() string { + return s.description +} + +func (s *baseSetting) GetDependency() string { + return s.dependsOn +} + +func (s *baseSetting) IsDisabled(foreignValue interface{}) bool { + return false +} diff --git a/server/public/pluginapi/experimental/panel/settings/bool_setting.go b/server/public/pluginapi/experimental/panel/settings/bool_setting.go new file mode 100644 index 00000000000..bb99d35287b --- /dev/null +++ b/server/public/pluginapi/experimental/panel/settings/bool_setting.go @@ -0,0 +1,114 @@ +package settings + +import ( + "errors" + "fmt" + + "github.com/mattermost/mattermost/server/public/model" +) + +type boolSetting struct { + baseSetting + store SettingStore +} + +// NewBoolSetting creates a new setting input for boolean values +func NewBoolSetting(id, title, description, dependsOn string, store SettingStore) Setting { + return &boolSetting{ + baseSetting: baseSetting{ + title: title, + description: description, + id: id, + dependsOn: dependsOn, + }, + store: store, + } +} + +func (s *boolSetting) Set(userID string, value interface{}) error { + boolValue := false + if value == TrueString { + boolValue = true + } + + err := s.store.SetSetting(userID, s.id, boolValue) + if err != nil { + return err + } + + return nil +} + +func (s *boolSetting) Get(userID string) (interface{}, error) { + value, err := s.store.GetSetting(userID, s.id) + if err != nil { + return "", err + } + boolValue, ok := value.(bool) + if !ok { + return "", errors.New("current value is not a bool") + } + + stringValue := FalseString + if boolValue { + stringValue = TrueString + } + + return stringValue, nil +} + +func (s *boolSetting) GetSlackAttachments(userID, settingHandler string, disabled bool) (*model.SlackAttachment, error) { + title := fmt.Sprintf("Setting: %s", s.title) + currentValueMessage := DisabledString + + actions := []*model.PostAction{} + if !disabled { + currentValue, err := s.Get(userID) + if err != nil { + return nil, err + } + + currentTextValue := "No" + if currentValue == TrueString { + currentTextValue = "Yes" + } + currentValueMessage = fmt.Sprintf("Current value: %s", currentTextValue) + + actionTrue := model.PostAction{ + Name: "Yes", + Integration: &model.PostActionIntegration{ + URL: settingHandler, + Context: map[string]interface{}{ + ContextIDKey: s.id, + ContextButtonValueKey: TrueString, + }, + }, + } + + actionFalse := model.PostAction{ + Name: "No", + Integration: &model.PostActionIntegration{ + URL: settingHandler, + Context: map[string]interface{}{ + ContextIDKey: s.id, + ContextButtonValueKey: FalseString, + }, + }, + } + actions = []*model.PostAction{&actionTrue, &actionFalse} + } + + text := fmt.Sprintf("%s\n%s", s.description, currentValueMessage) + sa := model.SlackAttachment{ + Title: title, + Text: text, + Fallback: fmt.Sprintf("%s: %s", title, text), + Actions: actions, + } + + return &sa, nil +} + +func (s *boolSetting) IsDisabled(foreignValue interface{}) bool { + return foreignValue == FalseString +} diff --git a/server/public/pluginapi/experimental/panel/settings/empty_setting.go b/server/public/pluginapi/experimental/panel/settings/empty_setting.go new file mode 100644 index 00000000000..61303a1960a --- /dev/null +++ b/server/public/pluginapi/experimental/panel/settings/empty_setting.go @@ -0,0 +1,41 @@ +package settings + +import ( + "fmt" + + "github.com/mattermost/mattermost/server/public/model" +) + +type emptySetting struct { + baseSetting +} + +// NewEmptySetting creates a new panel value with no setting attached +func NewEmptySetting(id, title, description string) Setting { + return &emptySetting{ + baseSetting: baseSetting{ + id: id, + title: title, + description: description, + }, + } +} + +func (s *emptySetting) GetSlackAttachments(userID, settingHandler string, disabled bool) (*model.SlackAttachment, error) { + title := fmt.Sprintf("Setting: %s", s.title) + sa := model.SlackAttachment{ + Title: title, + Text: s.description, + Fallback: fmt.Sprintf("%s: %s", title, s.description), + } + + return &sa, nil +} + +func (s *emptySetting) Get(userID string) (interface{}, error) { + return nil, nil +} + +func (s *emptySetting) Set(userID string, value interface{}) error { + return nil +} diff --git a/server/public/pluginapi/experimental/panel/settings/option_setting.go b/server/public/pluginapi/experimental/panel/settings/option_setting.go new file mode 100644 index 00000000000..5d908786419 --- /dev/null +++ b/server/public/pluginapi/experimental/panel/settings/option_setting.go @@ -0,0 +1,91 @@ +package settings + +import ( + "errors" + "fmt" + + "github.com/mattermost/mattermost/server/public/model" +) + +type optionSetting struct { + baseSetting + options []string + store SettingStore +} + +// NewOptionSetting creates a new setting input to select from a dropdown +func NewOptionSetting(id, title, description, dependsOn string, options []string, store SettingStore) Setting { + return &optionSetting{ + baseSetting: baseSetting{ + title: title, + description: description, + id: id, + dependsOn: dependsOn, + }, + options: options, + store: store, + } +} + +func (s *optionSetting) Set(userID string, value interface{}) error { + err := s.store.SetSetting(userID, s.id, value) + if err != nil { + return err + } + + return nil +} + +func (s *optionSetting) Get(userID string) (interface{}, error) { + value, err := s.store.GetSetting(userID, s.id) + if err != nil { + return "", err + } + valueString, ok := value.(string) + if !ok { + return "", errors.New("current value is not a string") + } + + return valueString, nil +} + +func (s *optionSetting) GetSlackAttachments(userID, settingHandler string, disabled bool) (*model.SlackAttachment, error) { + title := fmt.Sprintf("Setting: %s", s.title) + currentValueMessage := DisabledString + + actions := []*model.PostAction{} + if !disabled { + currentTextValue, err := s.Get(userID) + if err != nil { + return nil, err + } + currentValueMessage = fmt.Sprintf("Current value: %s", currentTextValue) + + actionOptions := model.PostAction{ + Name: "Select an option:", + Integration: &model.PostActionIntegration{ + URL: settingHandler + "?" + s.id + "=true", + Context: map[string]interface{}{ + ContextIDKey: s.id, + }, + }, + Type: "select", + Options: stringsToOptions(s.options), + } + + actions = []*model.PostAction{&actionOptions} + } + + text := fmt.Sprintf("%s\n%s", s.description, currentValueMessage) + sa := model.SlackAttachment{ + Title: title, + Text: text, + Fallback: fmt.Sprintf("%s: %s", title, text), + Actions: actions, + } + return &sa, nil +} + +func (s *optionSetting) IsDisabled(foreignValue interface{}) bool { + return foreignValue == FalseString +} diff --git a/server/public/pluginapi/experimental/panel/settings/read_only_setting.go b/server/public/pluginapi/experimental/panel/settings/read_only_setting.go new file mode 100644 index 00000000000..3604b4e5d30 --- /dev/null +++ b/server/public/pluginapi/experimental/panel/settings/read_only_setting.go @@ -0,0 +1,69 @@ +package settings + +import ( + "errors" + "fmt" + + "github.com/mattermost/mattermost/server/public/model" +) + +type readOnlySetting struct { + baseSetting + store SettingStore +} + +// NewReadOnlySetting creates a new panel value that only read from the setting +func NewReadOnlySetting(id, title, description, dependsOn string, store SettingStore) Setting { + return &readOnlySetting{ + baseSetting: baseSetting{ + title: title, + description: description, + id: id, + dependsOn: dependsOn, + }, + store: store, + } +} + +func (s *readOnlySetting) Get(userID string) (interface{}, error) { + value, err := s.store.GetSetting(userID, s.id) + if err != nil { + return "", err + } + stringValue, ok := value.(string) + if !ok { + return "", errors.New("current value is not a string") + } + + return stringValue, nil +} + +func (s *readOnlySetting) Set(userID string, value interface{}) error { + return nil +} + +func (s *readOnlySetting) GetSlackAttachments(userID, settingHandler string, disabled bool) (*model.SlackAttachment, error) { + title := fmt.Sprintf("Setting: %s", s.title) + currentValueMessage := DisabledString + + if !disabled { + currentValue, err := s.Get(userID) + if err != nil { + return nil, err + } + currentValueMessage = fmt.Sprintf("Current value: %s", currentValue) + } + + text := fmt.Sprintf("%s\n%s", s.description, currentValueMessage) + sa := model.SlackAttachment{ + Title: title, + Text: text, + Fallback: fmt.Sprintf("%s: %s", title, text), + } + + return &sa, nil +} + +func (s *readOnlySetting) IsDisabled(foreignValue interface{}) bool { + return foreignValue == FalseString +} diff --git a/server/public/pluginapi/experimental/panel/settings/setting.go b/server/public/pluginapi/experimental/panel/settings/setting.go new file mode 100644 index 00000000000..672ebdfd8f9 --- /dev/null +++ b/server/public/pluginapi/experimental/panel/settings/setting.go @@ -0,0 +1,33 @@ +package settings + +import ( + "github.com/mattermost/mattermost/server/public/model" +) + +const ( + // ContextIDKey defines the key used in the context to store the ID + ContextIDKey = "setting_id" + // ContextButtonValueKey defines the key used in the context to store a button value + ContextButtonValueKey = "button_value" + // ContextOptionValueKey defines the key used in the context to store a selected option value + ContextOptionValueKey = "selected_option" + + // DisabledString defines the string used to show that a setting is disabled + DisabledString = "Disabled" + // TrueString codify the boolean true into a string + TrueString = "true" + // FalseString codify the boolean false into a string + FalseString = "false" +) + +// Setting defines the behavior of each element a the panel +type Setting interface { + Set(userID string, value interface{}) error + Get(userID string) (interface{}, error) + GetID() string + GetDependency() string + IsDisabled(foreignValue interface{}) bool + GetTitle() string + GetDescription() string + GetSlackAttachments(userID, settingHandler string, disabled bool) (*model.SlackAttachment, error) +} diff --git a/server/public/pluginapi/experimental/panel/settings/store.go b/server/public/pluginapi/experimental/panel/settings/store.go new file mode 100644 index 00000000000..45afce6c742 --- /dev/null +++ b/server/public/pluginapi/experimental/panel/settings/store.go @@ -0,0 +1,7 @@ +package settings + +// SettingStore defines the behavior needed to set and get settings +type SettingStore interface { + SetSetting(userID, settingID string, value interface{}) error + GetSetting(userID, settingID string) (interface{}, error) +} diff --git a/server/public/pluginapi/experimental/panel/settings/utils.go b/server/public/pluginapi/experimental/panel/settings/utils.go new file mode 100644 index 00000000000..3a1cfe09512 --- /dev/null +++ b/server/public/pluginapi/experimental/panel/settings/utils.go @@ -0,0 +1,16 @@ +package settings + +import ( + "github.com/mattermost/mattermost/server/public/model" +) + +func stringsToOptions(in []string) []*model.PostActionOptions { + out := make([]*model.PostActionOptions, len(in)) + for i, o := range in { + out[i] = &model.PostActionOptions{ + Text: o, + Value: o, + } + } + return out +} diff --git a/server/public/pluginapi/experimental/panel/store.go b/server/public/pluginapi/experimental/panel/store.go new file mode 100644 index 00000000000..dabc1892c8f --- /dev/null +++ b/server/public/pluginapi/experimental/panel/store.go @@ -0,0 +1,53 @@ +package panel + +import ( + "errors" + + "github.com/mattermost/mattermost/server/public/pluginapi" +) + +type Store interface { + SetPanelPostID(userID string, postID string) error + GetPanelPostID(userID string) (string, error) + DeletePanelPostID(userID string) error +} + +type panelStore struct { + kv *pluginapi.KVService + keyPrefix string +} + +func NewPanelStore(kv *pluginapi.KVService, keyPrefix string) Store { + return &panelStore{ + kv: kv, + keyPrefix: keyPrefix, + } +} + +func (ps *panelStore) SetPanelPostID(userID, postID string) error { + ok, err := ps.kv.Set(ps.getKey(userID), postID) + if err != nil { + return err + } + if !ok { + return errors.New("value not set without errors") + } + return nil +} + +func (ps *panelStore) GetPanelPostID(userID string) (string, error) { + var postID string + err := ps.kv.Get(ps.getKey(userID), &postID) + if err != nil { + return "", err + } + return postID, nil +} + +func (ps *panelStore) DeletePanelPostID(userID string) error { + return ps.kv.Delete(ps.getKey(userID)) +} + +func (ps *panelStore) getKey(userID string) string { + return ps.keyPrefix + "-" + userID +} diff --git a/server/public/pluginapi/experimental/telemetry/doc.go b/server/public/pluginapi/experimental/telemetry/doc.go new file mode 100644 index 00000000000..5c39b63ae37 --- /dev/null +++ b/server/public/pluginapi/experimental/telemetry/doc.go @@ -0,0 +1,76 @@ +// Package telemetry allows you to add telemetry to your plugins. +// For Rudder, you can set the data plane URL and the write key on build time, +// to allow having different keys for production and development. +// If you are working on a Mattermost project, the data plane URL is already set. +// In order to default to the development key we have to set an environment variable during build time. +// Copy the following lines in build/custom.mk to setup that variable. +// +// ifndef MM_RUDDER_WRITE_KEY +// MM_RUDDER_WRITE_KEY = 1d5bMvdrfWClLxgK1FvV3s4U1tg +// endif +// +// To use this environment variable to set the key in the plugin, +// you have to add this line after the previous ones. +// +// LDFLAGS += -X "github.com/mattermost/mattermost/server/public/pluginapi/experimental/telemetry.rudderWriteKey=$(MM_RUDDER_WRITE_KEY)" +// +// MM_RUDDER_WRITE_KEY environment variable must be set also during CI +// to the production write key ("1dP7Oi78p0PK1brYLsfslgnbD1I"). +// If you want to use your own data plane URL, add also this line and +// make sure the MM_RUDDER_DATAPLANE_URL environment variable is set. +// +// LDFLAGS += -X "github.com/mattermost/mattermost/server/public/pluginapi/experimental/telemetry.rudderDataPlaneURL=$(MM_RUDDER_DATAPLANE_URL)" +// +// In order to use telemetry you should: +// +// 1. Add the new fields to the plugin +// +// type Plugin struct { +// plugin.MattermostPlugin +// ... +// telemetryClient telemetry.Client +// tracker telemetry.Tracker +// } +// +// 2. Start the telemetry client and tracker on plugin activate +// +// func (p *Plugin) OnActivate() error { +// p.telemetryClient, err = telemetry.NewRudderClient() +// if err != nil { +// p.API.LogWarn("telemetry client not started", "error", err.Error()) +// } +// ... +// p.tracker = telemetry.NewTracker( +// p.telemetryClient, +// p.API.GetDiagnosticId(), +// p.API.GetServerVersion(), +// Manifest.Id, +// Manifest.Version, +// "plugin_short_namame", +// telemetry.NewTrackerConfig(p.API.GetConfig()), +// logger.New(p.API) +// ) +// } +// +// 3. Trigger tracker changes when configuration changes +// +// func (p *Plugin) OnConfigurationChange() error { +// ... +// if p.tracker != nil { +// p.tracker.ReloadConfig(telemetry.NewTrackerConfig(p.API.GetConfig())) +// } +// return nil +// } +// +// 4. Close the client on plugin deactivate +// +// func (p *Plugin) OnDeactivate() error { +// if p.telemetryClient != nil { +// err := p.telemetryClient.Close() +// if err != nil { +// p.API.LogWarn("OnDeactivate: failed to close telemetryClient", "error", err.Error()) +// } +// } +// return nil +// } +package telemetry diff --git a/server/public/pluginapi/experimental/telemetry/rudder.go b/server/public/pluginapi/experimental/telemetry/rudder.go new file mode 100644 index 00000000000..76306977c65 --- /dev/null +++ b/server/public/pluginapi/experimental/telemetry/rudder.go @@ -0,0 +1,49 @@ +package telemetry + +import ( + rudder "github.com/rudderlabs/analytics-go" +) + +// rudderDataPlaneURL is set to the common Data Plane URL for all Mattermost Projects. +// It can be set during build time. More info in the package documentation. +var rudderDataPlaneURL = "https://pdat.matterlytics.com" + +// rudderWriteKey is set during build time. More info in the package documentation. +var rudderWriteKey string + +// NewRudderClient creates a new telemetry client with Rudder using the default configuration. +func NewRudderClient() (Client, error) { + return NewRudderClientWithCredentials(rudderWriteKey, rudderDataPlaneURL) +} + +// NewRudderClientWithCredentials lets you create a Rudder client with your own credentials. +func NewRudderClientWithCredentials(writeKey, dataPlaneURL string) (Client, error) { + client, err := rudder.NewWithConfig(writeKey, dataPlaneURL, rudder.Config{}) + if err != nil { + return nil, err + } + + return &rudderWrapper{client: client}, nil +} + +type rudderWrapper struct { + client rudder.Client +} + +func (r *rudderWrapper) Enqueue(t Track) error { + var context *rudder.Context + if t.InstallationID != "" { + context = &rudder.Context{Traits: map[string]any{"installationId": t.InstallationID}} + } + + return r.client.Enqueue(rudder.Track{ + UserId: t.UserID, + Event: t.Event, + Context: context, + Properties: t.Properties, + }) +} + +func (r *rudderWrapper) Close() error { + return r.client.Close() +} diff --git a/server/public/pluginapi/experimental/telemetry/tracker.go b/server/public/pluginapi/experimental/telemetry/tracker.go new file mode 100644 index 00000000000..ccb16c53261 --- /dev/null +++ b/server/public/pluginapi/experimental/telemetry/tracker.go @@ -0,0 +1,179 @@ +package telemetry + +import ( + "os" + "sync" + + "github.com/pkg/errors" + + "github.com/mattermost/mattermost/server/public/model" + "github.com/mattermost/mattermost/server/public/pluginapi/experimental/bot/logger" +) + +type TrackerConfig struct { + EnabledTracking bool + EnabledLogging bool +} + +// NewTrackerConfig returns a new trackerConfig from the current values of the model.Config. +func NewTrackerConfig(config *model.Config) TrackerConfig { + var enabledTracking, enabledLogging bool + if config == nil { + return TrackerConfig{} + } + + if enableDiagnostics := config.LogSettings.EnableDiagnostics; enableDiagnostics != nil { + enabledTracking = *enableDiagnostics + } + + if enableDeveloper := config.ServiceSettings.EnableDeveloper; enableDeveloper != nil { + enabledLogging = *enableDeveloper + } + + return TrackerConfig{ + EnabledTracking: enabledTracking, + EnabledLogging: enabledLogging, + } +} + +// Tracker defines a telemetry tracker +type Tracker interface { + // TrackEvent registers an event through the configured telemetry client + TrackEvent(event string, properties map[string]interface{}) error + // TrackUserEvent registers an event through the configured telemetry client associated to a user + TrackUserEvent(event string, userID string, properties map[string]interface{}) error + // Reload Config re-evaluates tracker config to determine if tracking behavior should change + ReloadConfig(config TrackerConfig) +} + +// Client defines a telemetry client +type Client interface { + // Enqueue adds a tracker event (Track) to be registered + Enqueue(t Track) error + // Close closes the client connection, flushing any event left on the queue + Close() error +} + +// Track defines an event ready for the client to process +type Track struct { + UserID string + Event string + Properties map[string]interface{} + InstallationID string +} + +type tracker struct { + client Client + diagnosticID string + serverVersion string + pluginID string + pluginVersion string + telemetryShortName string + configLock sync.RWMutex + config TrackerConfig + logger logger.Logger +} + +// NewTracker creates a default Tracker +// - c Client: A telemetry client. If nil, the tracker will not track any event. +// - diagnosticID: Server unique ID used for telemetry. +// - severVersion: Mattermost server version. +// - pluginID: The plugin ID. +// - pluginVersion: The plugin version. +// - telemetryShortName: Short name for the plugin to use in telemetry. Used to avoid dot separated names like `com.company.pluginName`. +// If a empty string is provided, it will use the pluginID. +// - config: Whether the system has enabled sending telemetry data. If false, the tracker will not track any event. +// - l Logger: A logger to debug event tracking and some important changes (it won't log if nil is passed as logger). +func NewTracker( + c Client, + diagnosticID, + serverVersion, + pluginID, + pluginVersion, + telemetryShortName string, + config TrackerConfig, + l logger.Logger, +) Tracker { + if telemetryShortName == "" { + telemetryShortName = pluginID + } + return &tracker{ + telemetryShortName: telemetryShortName, + client: c, + diagnosticID: diagnosticID, + serverVersion: serverVersion, + pluginID: pluginID, + pluginVersion: pluginVersion, + logger: l, + config: config, + } +} + +func (t *tracker) ReloadConfig(config TrackerConfig) { + t.configLock.Lock() + defer t.configLock.Unlock() + + if config.EnabledTracking != t.config.EnabledTracking { + if config.EnabledTracking { + t.debugf("Enabling plugin telemetry") + } else { + t.debugf("Disabling plugin telemetry") + } + } + + t.config.EnabledTracking = config.EnabledTracking + t.config.EnabledLogging = config.EnabledLogging +} + +// Note that config lock is handled by the caller. +func (t *tracker) debugf(message string, args ...interface{}) { + if t.logger == nil || !t.config.EnabledLogging { + return + } + t.logger.Debugf(message, args...) +} + +func (t *tracker) TrackEvent(event string, properties map[string]interface{}) error { + t.configLock.RLock() + defer t.configLock.RUnlock() + + event = t.telemetryShortName + "_" + event + if !t.config.EnabledTracking || t.client == nil { + t.debugf("Plugin telemetry event `%s` tracked, but not sent due to configuration", event) + return nil + } + + if properties == nil { + properties = map[string]interface{}{} + } + properties["PluginID"] = t.pluginID + properties["PluginVersion"] = t.pluginVersion + properties["ServerVersion"] = t.serverVersion + + // if we are part of a cloud installation, add it's ID to the tracked event's context. + installationID := os.Getenv("MM_CLOUD_INSTALLATION_ID") + + err := t.client.Enqueue(Track{ + // We consider the server the "user" on the telemetry system. Any reference to the actual user is passed by properties. + UserID: t.diagnosticID, + Event: event, + Properties: properties, + InstallationID: installationID, + }) + + if err != nil { + return errors.Wrap(err, "cannot enqueue the track") + } + t.debugf("Tracked plugin telemetry event `%s`", event) + + return nil +} + +func (t *tracker) TrackUserEvent(event, userID string, properties map[string]interface{}) error { + if properties == nil { + properties = map[string]interface{}{} + } + + properties["UserActualID"] = userID + return t.TrackEvent(event, properties) +} diff --git a/server/public/pluginapi/file.go b/server/public/pluginapi/file.go new file mode 100644 index 00000000000..5e33627e0e8 --- /dev/null +++ b/server/public/pluginapi/file.go @@ -0,0 +1,93 @@ +package pluginapi + +import ( + "bytes" + "io" + + "github.com/mattermost/mattermost/server/public/model" + "github.com/mattermost/mattermost/server/public/plugin" +) + +// FileService exposes methods to manipulate files, most often as post attachments. +type FileService struct { + api plugin.API +} + +// Get gets content of a file by id. +// +// Minimum server version: 5.8 +func (f *FileService) Get(id string) (io.Reader, error) { + contentBytes, appErr := f.api.GetFile(id) + if appErr != nil { + return nil, normalizeAppErr(appErr) + } + + return bytes.NewReader(contentBytes), nil +} + +// GetByPath reads a file by its path on the dist. +// +// Minimum server version: 5.3 +func (f *FileService) GetByPath(path string) (io.Reader, error) { + contentBytes, appErr := f.api.ReadFile(path) + if appErr != nil { + return nil, normalizeAppErr(appErr) + } + + return bytes.NewReader(contentBytes), nil +} + +// GetInfo gets a file's info by id. +// +// Minimum server version: 5.3 +func (f *FileService) GetInfo(id string) (*model.FileInfo, error) { + info, appErr := f.api.GetFileInfo(id) + + return info, normalizeAppErr(appErr) +} + +// SetSearchableContent update the File Info searchable text for full text search +// +// Minimum server version: 9.1 +func (f *FileService) SetSearchableContent(id string, content string) error { + appErr := f.api.SetFileSearchableContent(id, content) + return normalizeAppErr(appErr) +} + +// GetLink gets the public link of a file by id. +// +// Minimum server version: 5.6 +func (f *FileService) GetLink(id string) (string, error) { + link, appErr := f.api.GetFileLink(id) + + return link, normalizeAppErr(appErr) +} + +// Upload uploads a file to a channel to be later attached to a post. +// +// Minimum server version: 5.6 +func (f *FileService) Upload(content io.Reader, fileName, channelID string) (*model.FileInfo, error) { + contentBytes, err := io.ReadAll(content) + if err != nil { + return nil, err + } + + info, appErr := f.api.UploadFile(contentBytes, channelID, fileName) + + return info, normalizeAppErr(appErr) +} + +// CopyInfos duplicates the FileInfo objects referenced by the given file ids, recording +// the given user id as the new creator and returning the new set of file ids. +// +// The duplicate FileInfo objects are not initially linked to a post, but may now be passed +// on creation of a post. +// Use this API to duplicate a post and its file attachments without actually duplicating +// the uploaded files. +// +// Minimum server version: 5.2 +func (f *FileService) CopyInfos(ids []string, userID string) ([]string, error) { + newIDs, appErr := f.api.CopyFileInfos(userID, ids) + + return newIDs, normalizeAppErr(appErr) +} diff --git a/server/public/pluginapi/file_test.go b/server/public/pluginapi/file_test.go new file mode 100644 index 00000000000..fa15817006e --- /dev/null +++ b/server/public/pluginapi/file_test.go @@ -0,0 +1,185 @@ +package pluginapi_test + +import ( + "bytes" + "io" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/mattermost/mattermost/server/public/model" + "github.com/mattermost/mattermost/server/public/plugin/plugintest" + "github.com/mattermost/mattermost/server/public/pluginapi" +) + +func TestGetFile(t *testing.T) { + t.Run("success", func(t *testing.T) { + api := &plugintest.API{} + defer api.AssertExpectations(t) + client := pluginapi.NewClient(api, &plugintest.Driver{}) + + api.On("GetFile", "1").Return([]byte{2}, nil) + + content, err := client.File.Get("1") + require.NoError(t, err) + contentBytes, err := io.ReadAll(content) + require.NoError(t, err) + require.Equal(t, []byte{2}, contentBytes) + }) + + t.Run("failure", func(t *testing.T) { + api := &plugintest.API{} + defer api.AssertExpectations(t) + client := pluginapi.NewClient(api, &plugintest.Driver{}) + + appErr := newAppError() + + api.On("GetFile", "1").Return(nil, appErr) + + content, err := client.File.Get("1") + require.Equal(t, appErr, err) + require.Zero(t, content) + }) +} + +func TestGetFileByPath(t *testing.T) { + t.Run("success", func(t *testing.T) { + api := &plugintest.API{} + defer api.AssertExpectations(t) + client := pluginapi.NewClient(api, &plugintest.Driver{}) + + api.On("ReadFile", "1").Return([]byte{2}, nil) + + content, err := client.File.GetByPath("1") + require.NoError(t, err) + contentBytes, err := io.ReadAll(content) + require.NoError(t, err) + require.Equal(t, []byte{2}, contentBytes) + }) + + t.Run("failure", func(t *testing.T) { + api := &plugintest.API{} + defer api.AssertExpectations(t) + client := pluginapi.NewClient(api, &plugintest.Driver{}) + + appErr := newAppError() + + api.On("ReadFile", "1").Return(nil, appErr) + + content, err := client.File.GetByPath("1") + require.Equal(t, appErr, err) + require.Zero(t, content) + }) +} + +func TestGetFileInfo(t *testing.T) { + t.Run("success", func(t *testing.T) { + api := &plugintest.API{} + defer api.AssertExpectations(t) + client := pluginapi.NewClient(api, &plugintest.Driver{}) + + api.On("GetFileInfo", "1").Return(&model.FileInfo{Id: "2"}, nil) + + info, err := client.File.GetInfo("1") + require.NoError(t, err) + require.Equal(t, "2", info.Id) + }) + + t.Run("failure", func(t *testing.T) { + api := &plugintest.API{} + defer api.AssertExpectations(t) + client := pluginapi.NewClient(api, &plugintest.Driver{}) + + appErr := newAppError() + + api.On("GetFileInfo", "1").Return(nil, appErr) + + info, err := client.File.GetInfo("1") + require.Equal(t, appErr, err) + require.Zero(t, info) + }) +} + +func TestGetFileLink(t *testing.T) { + t.Run("success", func(t *testing.T) { + api := &plugintest.API{} + defer api.AssertExpectations(t) + client := pluginapi.NewClient(api, &plugintest.Driver{}) + + api.On("GetFileLink", "1").Return("2", nil) + + link, err := client.File.GetLink("1") + require.NoError(t, err) + require.Equal(t, "2", link) + }) + + t.Run("failure", func(t *testing.T) { + api := &plugintest.API{} + defer api.AssertExpectations(t) + client := pluginapi.NewClient(api, &plugintest.Driver{}) + + appErr := newAppError() + + api.On("GetFileLink", "1").Return("", appErr) + + link, err := client.File.GetLink("1") + require.Equal(t, appErr, err) + require.Zero(t, link) + }) +} + +func TestUploadFile(t *testing.T) { + t.Run("success", func(t *testing.T) { + api := &plugintest.API{} + defer api.AssertExpectations(t) + client := pluginapi.NewClient(api, &plugintest.Driver{}) + + api.On("UploadFile", []byte{1}, "3", "2").Return(&model.FileInfo{Id: "4"}, nil) + + info, err := client.File.Upload(bytes.NewReader([]byte{1}), "2", "3") + require.NoError(t, err) + require.Equal(t, "4", info.Id) + }) + + t.Run("failure", func(t *testing.T) { + api := &plugintest.API{} + defer api.AssertExpectations(t) + client := pluginapi.NewClient(api, &plugintest.Driver{}) + + appErr := newAppError() + + api.On("UploadFile", []byte{1}, "3", "2").Return(nil, appErr) + + info, err := client.File.Upload(bytes.NewReader([]byte{1}), "2", "3") + require.Equal(t, appErr, err) + require.Zero(t, info) + }) +} + +func TestCopyFileInfos(t *testing.T) { + t.Run("success", func(t *testing.T) { + api := &plugintest.API{} + defer api.AssertExpectations(t) + client := pluginapi.NewClient(api, &plugintest.Driver{}) + + api.On("CopyFileInfos", "3", []string{"1", "2"}).Return([]string{"4", "5"}, nil) + + newIDs, err := client.File.CopyInfos([]string{"1", "2"}, "3") + require.NoError(t, err) + require.Equal(t, []string{"4", "5"}, newIDs) + }) + + t.Run("failure", func(t *testing.T) { + api := &plugintest.API{} + defer api.AssertExpectations(t) + client := pluginapi.NewClient(api, &plugintest.Driver{}) + + appErr := newAppError() + + api.On("CopyFileInfos", "3", []string{"1", "2"}).Return(nil, appErr) + + newIDs, err := client.File.CopyInfos([]string{"1", "2"}, "3") + require.Equal(t, appErr, err) + require.Zero(t, newIDs) + }) +} diff --git a/server/public/pluginapi/frontend.go b/server/public/pluginapi/frontend.go new file mode 100644 index 00000000000..dc940598391 --- /dev/null +++ b/server/public/pluginapi/frontend.go @@ -0,0 +1,30 @@ +package pluginapi + +import ( + "github.com/mattermost/mattermost/server/public/model" + "github.com/mattermost/mattermost/server/public/plugin" +) + +// FrontendService exposes methods to interact with the frontend. +type FrontendService struct { + api plugin.API +} + +// OpenInteractiveDialog will open an interactive dialog on a user's client that +// generated the trigger ID. Used with interactive message buttons, menus +// and slash commands. +// +// Minimum server version: 5.6 +func (f *FrontendService) OpenInteractiveDialog(dialog model.OpenDialogRequest) error { + return normalizeAppErr(f.api.OpenInteractiveDialog(dialog)) +} + +// PublishWebSocketEvent sends an event to WebSocket connections. +// event is the type and will be prepended with "custom__". +// payload is the data sent with the event. Interface values must be primitive Go types or mattermost-server/model types. +// broadcast determines to which users to send the event. +// +// Minimum server version: 5.2 +func (f *FrontendService) PublishWebSocketEvent(event string, payload map[string]interface{}, broadcast *model.WebsocketBroadcast) { + f.api.PublishWebSocketEvent(event, payload, broadcast) +} diff --git a/server/public/pluginapi/group.go b/server/public/pluginapi/group.go new file mode 100644 index 00000000000..0aa9f01ca3d --- /dev/null +++ b/server/public/pluginapi/group.go @@ -0,0 +1,57 @@ +package pluginapi + +import ( + "github.com/mattermost/mattermost/server/public/model" + "github.com/mattermost/mattermost/server/public/plugin" +) + +// GroupService exposes methods to manipulate groups. +type GroupService struct { + api plugin.API +} + +// Get gets a group by ID. +// +// Minimum server version: 5.18 +func (g *GroupService) Get(groupID string) (*model.Group, error) { + group, appErr := g.api.GetGroup(groupID) + + return group, normalizeAppErr(appErr) +} + +// GetByName gets a group by name. +// +// Minimum server version: 5.18 +func (g *GroupService) GetByName(name string) (*model.Group, error) { + group, appErr := g.api.GetGroupByName(name) + + return group, normalizeAppErr(appErr) +} + +// GetMemberUsers gets a page of users from the given group. +// +// Minimum server version: 5.35 +func (g *GroupService) GetMemberUsers(groupID string, page, perPage int) ([]*model.User, error) { + users, appErr := g.api.GetGroupMemberUsers(groupID, page, perPage) + + return users, normalizeAppErr(appErr) +} + +// GetBySource gets a list of all groups for the given source. +// +// @tag Group +// Minimum server version: 5.35 +func (g *GroupService) GetBySource(groupSource model.GroupSource) ([]*model.Group, error) { + groups, appErr := g.api.GetGroupsBySource(groupSource) + + return groups, normalizeAppErr(appErr) +} + +// ListForUser gets the groups a user is in. +// +// Minimum server version: 5.18 +func (g *GroupService) ListForUser(userID string) ([]*model.Group, error) { + groups, appErr := g.api.GetGroupsForUser(userID) + + return groups, normalizeAppErr(appErr) +} diff --git a/server/public/pluginapi/i18n/doc.go b/server/public/pluginapi/i18n/doc.go new file mode 100644 index 00000000000..aede6b94ccb --- /dev/null +++ b/server/public/pluginapi/i18n/doc.go @@ -0,0 +1,2 @@ +// package i18n provides methods to read translations files and localize strings. +package i18n diff --git a/server/public/pluginapi/i18n/i18n.go b/server/public/pluginapi/i18n/i18n.go new file mode 100644 index 00000000000..26447c0adb5 --- /dev/null +++ b/server/public/pluginapi/i18n/i18n.go @@ -0,0 +1,136 @@ +package i18n + +import ( + "encoding/json" + "os" + "path/filepath" + "strings" + + "github.com/nicksnyder/go-i18n/v2/i18n" + "github.com/pkg/errors" + "golang.org/x/text/language" + + "github.com/mattermost/mattermost/server/public/model" +) + +// PluginAPI is the plugin API interface required to manage translations. +type PluginAPI interface { + GetBundlePath() (string, error) + GetConfig() *model.Config + GetUser(userID string) (*model.User, *model.AppError) + LogWarn(msg string, keyValuePairs ...interface{}) +} + +// Message is a string that can be localized. +// +// See https://pkg.go.dev/github.com/nicksnyder/go-i18n/v2/i18n?tab=doc#Message for more details. +type Message = i18n.Message + +// LocalizeConfig configures a call to the Localize method on Localizer. +// +// See https://pkg.go.dev/github.com/nicksnyder/go-i18n/v2/i18n?tab=doc#LocalizeConfig for more details. +type LocalizeConfig = i18n.LocalizeConfig + +// Localizer provides Localize and MustLocalize methods that return localized messages. +// +// See https://pkg.go.dev/github.com/nicksnyder/go-i18n/v2/i18n?tab=doc#Localizer for more details. +type Localizer = i18n.Localizer + +// Bundle stores a set of messages and pluralization rules. +// Most plugins only need a single bundle +// that is initialized on activation. +// It is not goroutine safe to modify the bundle while Localizers +// are reading from it. +type Bundle struct { + *i18n.Bundle + api PluginAPI +} + +// InitBundle loads all localization files from a given path into a bundle and return this. +// path is a relative path in the plugin bundle, e.g. assets/i18n. +// Every file except the ones named active.*.json. +// The default language is English. +func InitBundle(api PluginAPI, path string) (*Bundle, error) { + bundle := &Bundle{ + Bundle: i18n.NewBundle(language.English), + api: api, + } + bundle.RegisterUnmarshalFunc("json", json.Unmarshal) + + bundlePath, err := api.GetBundlePath() + if err != nil { + return nil, errors.Wrap(err, "failed to get bundle path") + } + + i18nDir := filepath.Join(bundlePath, path) + + files, err := os.ReadDir(i18nDir) + if err != nil { + return nil, errors.Wrap(err, "failed to open i18n directory") + } + + for _, file := range files { + if !strings.HasPrefix(file.Name(), "active.") { + continue + } + + if !strings.HasSuffix(file.Name(), ".json") { + continue + } + + if file.Name() == "active.en.json" { + continue + } + + _, err = bundle.LoadMessageFile(filepath.Join(i18nDir, file.Name())) + if err != nil { + return nil, errors.Wrapf(err, "failed to load message file %s", file.Name()) + } + } + + return bundle, nil +} + +// GetUserLocalizer returns a localizer that localizes in the users locale. +func (b *Bundle) GetUserLocalizer(userID string) *i18n.Localizer { + user, err := b.api.GetUser(userID) + if err != nil { + b.api.LogWarn("Failed get user's locale", "error", err.Error()) + return b.GetServerLocalizer() + } + + return i18n.NewLocalizer(b.Bundle, user.Locale) +} + +// GetServerLocalizer returns a localizer that localizes in the default server locale. +// +// This is useful for situations where a messages is shown to every user, +// independent of the users locale. +func (b *Bundle) GetServerLocalizer() *i18n.Localizer { + local := *b.api.GetConfig().LocalizationSettings.DefaultServerLocale + + return i18n.NewLocalizer(b.Bundle, local) +} + +// LocalizeDefaultMessage localizer the provided message. +// An empty string is returned when the localization fails. +func (b *Bundle) LocalizeDefaultMessage(l *Localizer, m *Message) string { + s, err := l.LocalizeMessage(m) + if err != nil { + b.api.LogWarn("Failed to localize message", "message ID", m.ID, "error", err.Error()) + return "" + } + + return s +} + +// LocalizeWithConfig localizer the provided localize config. +// An empty string is returned when the localization fails. +func (b *Bundle) LocalizeWithConfig(l *Localizer, lc *LocalizeConfig) string { + s, err := l.Localize(lc) + if err != nil { + b.api.LogWarn("Failed to localize with config", "error", err.Error()) + return "" + } + return s +} diff --git a/server/public/pluginapi/i18n/i18n_test.go b/server/public/pluginapi/i18n/i18n_test.go new file mode 100644 index 00000000000..aff327d2eaf --- /dev/null +++ b/server/public/pluginapi/i18n/i18n_test.go @@ -0,0 +1,296 @@ +package i18n_test + +import ( + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + "golang.org/x/text/language" + + "github.com/mattermost/mattermost/server/public/model" + "github.com/mattermost/mattermost/server/public/plugin" + "github.com/mattermost/mattermost/server/public/plugin/plugintest" + "github.com/mattermost/mattermost/server/public/pluginapi/i18n" +) + +//nolint:govet +func ExampleInitBundle() { + type Plugin struct { + plugin.MattermostPlugin + + b *i18n.Bundle + } + + p := Plugin{} + b, err := i18n.InitBundle(p.API, filepath.Join("assets", "i18n")) + if err != nil { + panic(err) + } + + p.b = b +} + +func TestInitBundle(t *testing.T) { + t.Run("fine", func(t *testing.T) { + dir, err := os.MkdirTemp("", "") + require.NoError(t, err) + + defer os.RemoveAll(dir) + + // Create assets/i18n dir + i18nDir := filepath.Join(dir, "assets", "i18n") + err = os.MkdirAll(i18nDir, 0o700) + require.NoError(t, err) + + file := filepath.Join(i18nDir, "active.de.json") + content := []byte("{}") + err = os.WriteFile(file, content, 0o600) + require.NoError(t, err) + + // Add en translation file. + // InitBundle should ignore it. + file = filepath.Join(i18nDir, "active.en.json") + content = []byte("") + err = os.WriteFile(file, content, 0o600) + require.NoError(t, err) + + // Add json junk file + file = filepath.Join(i18nDir, "foo.json") + content = []byte("") + err = os.WriteFile(file, content, 0o600) + require.NoError(t, err) + + // Add active. junk file + file = filepath.Join(i18nDir, "active.foo") + content = []byte("") + err = os.WriteFile(file, content, 0o600) + require.NoError(t, err) + + api := &plugintest.API{} + api.On("GetBundlePath").Return(dir, nil) + defer api.AssertExpectations(t) + + b, err := i18n.InitBundle(api, "assets/i18n") + assert.NoError(t, err) + assert.NotNil(t, b) + + assert.ElementsMatch(t, []language.Tag{language.English, language.German}, b.LanguageTags()) + }) + + t.Run("fine", func(t *testing.T) { + dir, err := os.MkdirTemp("", "") + require.NoError(t, err) + + defer os.RemoveAll(dir) + + // Create assets/i18n dir + i18nDir := filepath.Join(dir, "assets", "i18n") + err = os.MkdirAll(i18nDir, 0o700) + require.NoError(t, err) + + file := filepath.Join(i18nDir, "active.de.json") + content := []byte("{}") + err = os.WriteFile(file, content, 0o600) + require.NoError(t, err) + + // Add translation file with invalid content + file = filepath.Join(i18nDir, "active.es.json") + content = []byte("foo bar") + err = os.WriteFile(file, content, 0o600) + require.NoError(t, err) + + api := &plugintest.API{} + api.On("GetBundlePath").Return(dir, nil) + defer api.AssertExpectations(t) + + b, err := i18n.InitBundle(api, "assets/i18n") + assert.Error(t, err) + assert.Nil(t, b) + }) +} + +func TestLocalizeDefaultMessage(t *testing.T) { + t.Run("fine", func(t *testing.T) { + api := &plugintest.API{} + defaultServerLocale := "en" + api.On("GetConfig").Return(&model.Config{ + LocalizationSettings: model.LocalizationSettings{ + DefaultServerLocale: &defaultServerLocale, + }, + }) + api.On("GetBundlePath").Return(".", nil) + defer api.AssertExpectations(t) + + b, err := i18n.InitBundle(api, ".") + require.NoError(t, err) + + l := b.GetServerLocalizer() + m := &i18n.Message{ + Other: "test message", + } + + assert.Equal(t, m.Other, b.LocalizeDefaultMessage(l, m)) + }) + + t.Run("empty message", func(t *testing.T) { + api := &plugintest.API{} + defaultServerLocale := "en" + api.On("GetConfig").Return(&model.Config{ + LocalizationSettings: model.LocalizationSettings{ + DefaultServerLocale: &defaultServerLocale, + }, + }) + api.On("GetBundlePath").Return(".", nil) + api.On("LogWarn", mock.AnythingOfType("string"), + mock.AnythingOfType("string"), mock.AnythingOfType("string"), + mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return() + defer api.AssertExpectations(t) + + b, err := i18n.InitBundle(api, ".") + require.NoError(t, err) + + l := b.GetServerLocalizer() + m := &i18n.Message{} + + assert.Equal(t, "", b.LocalizeDefaultMessage(l, m)) + }) +} + +func TestLocalizeWithConfig(t *testing.T) { + t.Run("fine", func(t *testing.T) { + api := &plugintest.API{} + defaultServerLocale := "en" + api.On("GetConfig").Return(&model.Config{ + LocalizationSettings: model.LocalizationSettings{ + DefaultServerLocale: &defaultServerLocale, + }, + }) + api.On("GetBundlePath").Return(".", nil) + defer api.AssertExpectations(t) + + b, err := i18n.InitBundle(api, ".") + require.NoError(t, err) + + l := b.GetServerLocalizer() + lc := &i18n.LocalizeConfig{ + DefaultMessage: &i18n.Message{ + Other: "test messsage", + }, + } + + assert.Equal(t, lc.DefaultMessage.Other, b.LocalizeWithConfig(l, lc)) + }) + + t.Run("empty config", func(t *testing.T) { + api := &plugintest.API{} + defaultServerLocale := "en" + api.On("GetConfig").Return(&model.Config{ + LocalizationSettings: model.LocalizationSettings{ + DefaultServerLocale: &defaultServerLocale, + }, + }) + api.On("GetBundlePath").Return(".", nil) + api.On("LogWarn", mock.AnythingOfType("string"), + mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return() + defer api.AssertExpectations(t) + + b, err := i18n.InitBundle(api, ".") + require.NoError(t, err) + + l := b.GetServerLocalizer() + lc := &i18n.LocalizeConfig{} + + assert.Equal(t, "", b.LocalizeWithConfig(l, lc)) + }) + + t.Run("empty message", func(t *testing.T) { + api := &plugintest.API{} + defaultServerLocale := "en" + api.On("GetConfig").Return(&model.Config{ + LocalizationSettings: model.LocalizationSettings{ + DefaultServerLocale: &defaultServerLocale, + }, + }) + api.On("GetBundlePath").Return(".", nil) + api.On("LogWarn", mock.AnythingOfType("string"), + mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return() + defer api.AssertExpectations(t) + + b, err := i18n.InitBundle(api, ".") + require.NoError(t, err) + + l := b.GetServerLocalizer() + lc := &i18n.LocalizeConfig{ + DefaultMessage: &i18n.Message{}, + } + + assert.Equal(t, "", b.LocalizeWithConfig(l, lc)) + }) +} +func TestGetUserLocalizer(t *testing.T) { + t.Run("fine", func(t *testing.T) { + api := &plugintest.API{} + api.On("GetUser", "userID").Return(&model.User{ + Locale: "de", + }, nil) + api.On("GetBundlePath").Return(".", nil) + defer api.AssertExpectations(t) + + b, err := i18n.InitBundle(api, ".") + require.NoError(t, err) + + l := b.GetUserLocalizer("userID") + assert.NotNil(t, l) + + enMessage := &i18n.Message{ + Other: "a", + } + + deMessage := &i18n.Message{ + Other: "b", + } + + err = b.Bundle.AddMessages(language.German, deMessage) + require.NoError(t, err) + + assert.Equal(t, deMessage.Other, b.LocalizeDefaultMessage(l, enMessage)) + }) + + t.Run("error", func(t *testing.T) { + api := &plugintest.API{} + defaultServerLocale := "es" + api.On("GetConfig").Return(&model.Config{ + LocalizationSettings: model.LocalizationSettings{ + DefaultServerLocale: &defaultServerLocale, + }, + }) + api.On("GetBundlePath").Return(".", nil) + api.On("GetUser", "userID").Return(nil, &model.AppError{}) + api.On("LogWarn", mock.AnythingOfType("string"), + mock.AnythingOfType("string"), mock.AnythingOfType("string"), + mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return() + defer api.AssertExpectations(t) + + b, err := i18n.InitBundle(api, ".") + require.NoError(t, err) + + l := b.GetUserLocalizer("userID") + assert.NotNil(t, l) + + enMessage := &i18n.Message{ + Other: "a", + } + + esMessage := &i18n.Message{ + Other: "b", + } + + err = b.Bundle.AddMessages(language.Spanish, esMessage) + require.NoError(t, err) + + assert.Equal(t, esMessage.Other, b.LocalizeDefaultMessage(l, enMessage)) + }) +} diff --git a/server/public/pluginapi/kv.go b/server/public/pluginapi/kv.go new file mode 100644 index 00000000000..ce7f9a53718 --- /dev/null +++ b/server/public/pluginapi/kv.go @@ -0,0 +1,319 @@ +package pluginapi + +import ( + "encoding/json" + "fmt" + "strings" + "time" + + "github.com/pkg/errors" + + "github.com/mattermost/mattermost/server/public/model" + "github.com/mattermost/mattermost/server/public/plugin" +) + +// numRetries is the number of times the setAtomicWithRetries will retry before returning an error. +const numRetries = 5 + +// KVService exposes methods to read and write key-value pairs for the active plugin. +// +// This service cannot be used to read or write key-value pairs for other plugins. +type KVService struct { + api plugin.API +} + +// TODO: Should this be un exported? +type KVSetOptions struct { + model.PluginKVSetOptions + oldValue interface{} +} + +// KVSetOption is an option passed to Set() operation. +type KVSetOption func(*KVSetOptions) + +// SetAtomic guarantees the write will occur only when the current value of matches the given old +// value. A client is expected to read the old value first, then pass it back to ensure the value +// has not since been modified. +func SetAtomic(oldValue interface{}) KVSetOption { + return func(o *KVSetOptions) { + o.Atomic = true + o.oldValue = oldValue + } +} + +// SetExpiry configures a key value to expire after the given duration relative to now. +func SetExpiry(ttl time.Duration) KVSetOption { + return func(o *KVSetOptions) { + o.ExpireInSeconds = int64(ttl / time.Second) + } +} + +// Set stores a key-value pair, unique per plugin. +// Keys prefixed with `mmi_` are reserved for use by this package and will fail to be set. +// +// Returns (false, err) if DB error occurred +// Returns (false, nil) if the value was not set +// Returns (true, nil) if the value was set +// +// Minimum server version: 5.18 +func (k *KVService) Set(key string, value interface{}, options ...KVSetOption) (bool, error) { + if strings.HasPrefix(key, "mmi_") { + return false, errors.New("'mmi_' prefix is not allowed for keys") + } + + opts := KVSetOptions{} + for _, o := range options { + o(&opts) + } + + var valueBytes []byte + if value != nil { + // Assume JSON encoding, unless explicitly given a byte slice. + var isValueInBytes bool + valueBytes, isValueInBytes = value.([]byte) + if !isValueInBytes { + var err error + valueBytes, err = json.Marshal(value) + if err != nil { + return false, errors.Wrapf(err, "failed to marshal value %v", value) + } + } + } + + downstreamOpts := model.PluginKVSetOptions{ + Atomic: opts.Atomic, + ExpireInSeconds: opts.ExpireInSeconds, + } + + if opts.oldValue != nil { + oldValueBytes, isOldValueInBytes := opts.oldValue.([]byte) + if isOldValueInBytes { + downstreamOpts.OldValue = oldValueBytes + } else { + data, err := json.Marshal(opts.oldValue) + if err != nil { + return false, errors.Wrapf(err, "failed to marshal value %v", opts.oldValue) + } + + downstreamOpts.OldValue = data + } + } + + written, appErr := k.api.KVSetWithOptions(key, valueBytes, downstreamOpts) + return written, normalizeAppErr(appErr) +} + +// SetWithExpiry sets a key-value pair with the given expiration duration relative to now. +// +// Deprecated: SetWithExpiry exists to streamline adoption of this package for existing plugins. +// Use Set with the appropriate options instead. +// +// Minimum server version: 5.18 +func (k *KVService) SetWithExpiry(key string, value interface{}, ttl time.Duration) error { + _, err := k.Set(key, value, SetExpiry(ttl)) + + return err +} + +// CompareAndSet writes a key-value pair if the current value matches the given old value. +// +// Returns (false, err) if DB error occurred +// Returns (false, nil) if the value was not set +// Returns (true, nil) if the value was set +// +// Deprecated: CompareAndSet exists to streamline adoption of this package for existing plugins. +// Use Set with the appropriate options instead. +// +// Minimum server version: 5.18 +func (k *KVService) CompareAndSet(key string, oldValue, value interface{}) (bool, error) { + return k.Set(key, value, SetAtomic(oldValue)) +} + +// CompareAndDelete deletes a key-value pair if the current value matches the given old value. +// +// Returns (false, err) if DB error occurred +// Returns (false, nil) if current value != oldValue or key does not exist when deleting +// Returns (true, nil) if current value == oldValue and the key was deleted +// +// Deprecated: CompareAndDelete exists to streamline adoption of this package for existing plugins. +// Use Set with the appropriate options instead. +// +// Minimum server version: 5.18 +func (k *KVService) CompareAndDelete(key string, oldValue interface{}) (bool, error) { + return k.Set(key, nil, SetAtomic(oldValue)) +} + +// SetAtomicWithRetries will set a key-value pair atomically using compare and set semantics: +// it will read key's value (to get oldValue), perform valueFunc (to get newValue), +// and compare and set (comparing oldValue and setting newValue). +// +// Parameters: +// +// `key` is the key to get and set. +// `valueFunc` is a user-provided function that will take the old value as a []byte and +// return the new value or an error. If valueFunc needs to operate on +// oldValue, it will need to use the oldValue as a []byte, or convert +// oldValue into the expected type (e.g., by parsing it, or marshaling it +// into the expected struct). It should then return the newValue as the type +// expected to be stored. +// +// Returns: +// +// Returns err if the key could not be retrieved (DB error), valueFunc returned an error, +// if the key could not be set (DB error), or if the key could not be set (after retries). +// Returns nil if the value was set. +// +// Minimum server version: 5.18 +func (k *KVService) SetAtomicWithRetries(key string, valueFunc func(oldValue []byte) (newValue interface{}, err error)) error { + for i := 0; i < numRetries; i++ { + var oldVal []byte + if err := k.Get(key, &oldVal); err != nil { + return errors.Wrapf(err, "failed to get value for key %s", key) + } + + newVal, err := valueFunc(oldVal) + if err != nil { + return errors.Wrap(err, "valueFunc failed") + } + + if saved, err := k.Set(key, newVal, SetAtomic(oldVal)); err != nil { + return errors.Wrapf(err, "DB failed to set value for key %s", key) + } else if saved { + return nil + } + + // small delay to allow cooperative scheduling to do its thing + time.Sleep(10 * time.Millisecond) + } + return fmt.Errorf("failed to set value after %d retries", numRetries) +} + +// Get gets the value for the given key into the given interface. +// +// An error is returned only if the value cannot be fetched. A non-existent key will return no +// error, with nothing written to the given interface. +// +// Minimum server version: 5.2 +func (k *KVService) Get(key string, o interface{}) error { + data, appErr := k.api.KVGet(key) + if appErr != nil { + return normalizeAppErr(appErr) + } + + if len(data) == 0 { + return nil + } + + if bytesOut, ok := o.(*[]byte); ok { + *bytesOut = data + return nil + } + + if err := json.Unmarshal(data, o); err != nil { + return errors.Wrapf(err, "failed to unmarshal value for key %s", key) + } + + return nil +} + +// Delete deletes the given key-value pair. +// +// An error is returned only if the value failed to be deleted. A non-existent key will return +// no error. +// +// Minimum server version: 5.18 +func (k *KVService) Delete(key string) error { + _, err := k.Set(key, nil) + return err +} + +// DeleteAll removes all key-value pairs. +// +// Minimum server version: 5.6 +func (k *KVService) DeleteAll() error { + return normalizeAppErr(k.api.KVDeleteAll()) +} + +// ListKeysOption used to configure a ListKeys() operation. +type ListKeysOption func(*listKeysOptions) + +// listKeysOptions holds configurations of a ListKeys() operation. +type listKeysOptions struct { + checkers []func(key string) (keep bool, err error) +} + +func (o *listKeysOptions) checkAll(key string) (keep bool, err error) { + for _, check := range o.checkers { + keep, err := check(key) + if err != nil { + return false, err + } + if !keep { + return false, nil + } + } + + // key made it through all checkers + return true, nil +} + +// WithPrefix only return keys that start with the given string. +func WithPrefix(prefix string) ListKeysOption { + return WithChecker(func(key string) (keep bool, err error) { + return strings.HasPrefix(key, prefix), nil + }) +} + +// WithChecker allows for a custom filter function to determine which keys to return. +// Returning true will keep the key and false will filter it out. Returning an error +// will halt KVListWithOptions immediately and pass the error up (with no other results). +func WithChecker(f func(key string) (keep bool, err error)) ListKeysOption { + return func(args *listKeysOptions) { + args.checkers = append(args.checkers, f) + } +} + +// ListKeys lists all keys that match the given options. If no options are provided then all keys are returned. +// +// Minimum server version: 5.6 +func (k *KVService) ListKeys(page, count int, options ...ListKeysOption) ([]string, error) { + // convert functional options into args struct + args := &listKeysOptions{ + checkers: nil, + } + for _, opt := range options { + opt(args) + } + + // get our keys a batch at a time, filter out the ones we don't want based on our args + // any errors will hault the whole process and return the error raw + + keys, appErr := k.api.KVList(page, count) + if appErr != nil { + return nil, normalizeAppErr(appErr) + } + + if len(args.checkers) == 0 { + // no checkers, just return the keys + return keys, nil + } + + ret := make([]string, 0) + // we have a filter, so check each key, all checkers must say key + // for us to keep a key + for _, key := range keys { + keep, err := args.checkAll(key) + if err != nil { + return nil, err + } + + if !keep { + continue + } + + // didn't get filtered out, add to our return + ret = append(ret, key) + } + + return ret, nil +} diff --git a/server/public/pluginapi/kv_test.go b/server/public/pluginapi/kv_test.go new file mode 100644 index 00000000000..f0e43d9e79f --- /dev/null +++ b/server/public/pluginapi/kv_test.go @@ -0,0 +1,681 @@ +package pluginapi_test + +import ( + "encoding/json" + "errors" + "net/http" + "strconv" + "strings" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/mattermost/mattermost/server/public/model" + "github.com/mattermost/mattermost/server/public/plugin/plugintest" + "github.com/mattermost/mattermost/server/public/pluginapi" +) + +func newAppError() *model.AppError { + return model.NewAppError("here", "id", nil, "an error occurred", http.StatusInternalServerError) +} + +func TestKVSet(t *testing.T) { + tests := []struct { + name string + key string + value interface{} + options []pluginapi.KVSetOption + expectedValue []byte + expectedOptions model.PluginKVSetOptions + upserted bool + err error + }{ + { + "[]byte value", + "1", + 2, + []pluginapi.KVSetOption{}, + []byte(`2`), + model.PluginKVSetOptions{}, + true, + nil, + }, { + "string value", + "1", + "2", + []pluginapi.KVSetOption{}, + []byte(`"2"`), + model.PluginKVSetOptions{}, + true, + nil, + }, { + "struct value", + "1", + struct{ A string }{"2"}, + []pluginapi.KVSetOption{}, + []byte(`{"A":"2"}`), + model.PluginKVSetOptions{}, + true, + nil, + }, { + "compare and set []byte value", + "1", + []byte{2}, + []pluginapi.KVSetOption{ + pluginapi.SetAtomic([]byte{3}), + }, + []byte{2}, + model.PluginKVSetOptions{ + Atomic: true, + OldValue: []byte{3}, + }, + true, + nil, + }, { + "compare and set string value", + "1", + "2", + []pluginapi.KVSetOption{ + pluginapi.SetAtomic("3"), + }, + []byte(`"2"`), + model.PluginKVSetOptions{ + Atomic: true, + OldValue: []byte(`"3"`), + }, true, + nil, + }, { + "value is nil", + "1", + nil, + []pluginapi.KVSetOption{}, + nil, + model.PluginKVSetOptions{}, + true, + nil, + }, { + "current value is nil", + "1", + "2", + []pluginapi.KVSetOption{ + pluginapi.SetAtomic(nil), + }, + []byte(`"2"`), + model.PluginKVSetOptions{ + Atomic: true, + OldValue: nil, + }, + true, + nil, + }, { + "value is nil, current value is []byte", + "1", + nil, + []pluginapi.KVSetOption{ + pluginapi.SetAtomic([]byte{3}), + }, + nil, + model.PluginKVSetOptions{ + Atomic: true, + OldValue: []byte{3}, + }, + true, + nil, + }, { + "error", + "1", + []byte{2}, + []pluginapi.KVSetOption{}, + []byte{2}, + model.PluginKVSetOptions{}, + false, + newAppError(), + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + api := &plugintest.API{} + client := pluginapi.NewClient(api, &plugintest.Driver{}) + + api.On("KVSetWithOptions", test.key, test.expectedValue, test.expectedOptions).Return(test.upserted, test.err) + + upserted, err := client.KV.Set(test.key, test.value, test.options...) + if test.err != nil { + require.Error(t, err, test.name) + require.False(t, upserted, test.name) + } else { + require.NoError(t, err, test.name) + assert.True(t, upserted, test.name) + } + api.AssertExpectations(t) + }) + } +} + +func TestSetWithExpiry(t *testing.T) { + api := &plugintest.API{} + defer api.AssertExpectations(t) + client := pluginapi.NewClient(api, &plugintest.Driver{}) + + api.On("KVSetWithOptions", "1", []byte(`2`), model.PluginKVSetOptions{ + ExpireInSeconds: 60, + }).Return(true, nil) + + err := client.KV.SetWithExpiry("1", 2, time.Minute) + require.NoError(t, err) +} + +func TestCompareAndSet(t *testing.T) { + api := &plugintest.API{} + defer api.AssertExpectations(t) + client := pluginapi.NewClient(api, &plugintest.Driver{}) + + api.On("KVSetWithOptions", "1", []byte("2"), model.PluginKVSetOptions{ + Atomic: true, + OldValue: []byte("3"), + }).Return(true, nil) + + upserted, err := client.KV.CompareAndSet("1", 3, 2) + require.NoError(t, err) + assert.True(t, upserted) +} + +func TestCompareAndDelete(t *testing.T) { + api := &plugintest.API{} + defer api.AssertExpectations(t) + client := pluginapi.NewClient(api, &plugintest.Driver{}) + + api.On("KVSetWithOptions", "1", []byte(nil), model.PluginKVSetOptions{ + Atomic: true, + OldValue: []byte("2"), + }).Return(true, nil) + + deleted, err := client.KV.CompareAndDelete("1", 2) + require.NoError(t, err) + assert.True(t, deleted) +} + +func TestSetAtomicWithRetries(t *testing.T) { + tests := []struct { + name string + key string + valueFunc func(t *testing.T) func(old []byte) (interface{}, error) + setupAPI func(api *plugintest.API) + wantErr bool + expectedErrPrefix string + }{ + { + name: "Test SetAtomicWithRetries success after first attempt", + key: "testNum", + valueFunc: func(t *testing.T) func(old []byte) (interface{}, error) { + return func(old []byte) (interface{}, error) { + return 2, nil + } + }, + setupAPI: func(api *plugintest.API) { + oldJSONBytes, _ := json.Marshal(1) + newJSONBytes, _ := json.Marshal(2) + api.On("KVGet", "testNum").Return(oldJSONBytes, nil) + api.On("KVSetWithOptions", "testNum", newJSONBytes, model.PluginKVSetOptions{ + Atomic: true, + OldValue: oldJSONBytes, + }).Return(true, nil) + }, + }, + { + name: "Test success after first attempt, old is struct and as expected", + key: "testNum2", + valueFunc: func(t *testing.T) func(old []byte) (interface{}, error) { + return func(old []byte) (interface{}, error) { + type toStore struct { + Value int + } + var fromDB toStore + if err := json.Unmarshal(old, &fromDB); err != nil { + return nil, err + } + require.Equal(t, 1, fromDB.Value, "old not as expected") + return toStore{2}, nil + } + }, + setupAPI: func(api *plugintest.API) { + type toStore struct { + Value int + } + oldJSONBytes, _ := json.Marshal(toStore{1}) + newJSONBytes, _ := json.Marshal(toStore{2}) + api.On("KVGet", "testNum2").Return(oldJSONBytes, nil) + api.On("KVSetWithOptions", "testNum2", newJSONBytes, model.PluginKVSetOptions{ + Atomic: true, + OldValue: oldJSONBytes, + }).Return(true, nil) + }, + }, + { + name: "Test success after first attempt, old is an int value and as expected", + key: "testNum2", + valueFunc: func(t *testing.T) func(old []byte) (interface{}, error) { + return func(old []byte) (interface{}, error) { + fromDB, err := strconv.Atoi(string(old)) + if err != nil { + return nil, err + } + require.Equal(t, 1, fromDB, "old not as expected") + return 2, nil + } + }, + setupAPI: func(api *plugintest.API) { + oldJSONBytes, _ := json.Marshal(1) + newJSONBytes, _ := json.Marshal(2) + api.On("KVGet", "testNum2").Return(oldJSONBytes, nil) + api.On("KVSetWithOptions", "testNum2", newJSONBytes, model.PluginKVSetOptions{ + Atomic: true, + OldValue: oldJSONBytes, + }).Return(true, nil) + }, + }, + { + name: "Test SetAtomicWithRetries success on fourth attempt", + key: "testNum", + valueFunc: func(t *testing.T) func(old []byte) (interface{}, error) { + return func(old []byte) (interface{}, error) { + return 2, nil + } + }, + setupAPI: func(api *plugintest.API) { + oldJSONBytes, _ := json.Marshal(1) + newJSONBytes, _ := json.Marshal(2) + api.On("KVGet", "testNum").Return(oldJSONBytes, nil).Times(4) + api.On("KVSetWithOptions", "testNum", newJSONBytes, model.PluginKVSetOptions{ + Atomic: true, + OldValue: oldJSONBytes, + }).Return(false, nil).Times(3) + api.On("KVSetWithOptions", "testNum", newJSONBytes, model.PluginKVSetOptions{ + Atomic: true, + OldValue: oldJSONBytes, + }).Return(true, nil).Once() + }, + }, + { + name: "Test SetAtomicWithRetries success on fourth attempt because value was changed between calls to KVGet", + key: "testNum", + valueFunc: func(t *testing.T) func(old []byte) (interface{}, error) { + return func(old []byte) (interface{}, error) { + return 2, nil + } + }, + setupAPI: func(api *plugintest.API) { + oldJSONBytes, _ := json.Marshal(1) + newJSONBytes, _ := json.Marshal(2) + api.On("KVGet", "testNum").Return(oldJSONBytes, nil).Times(4) + api.On("KVSetWithOptions", "testNum", newJSONBytes, model.PluginKVSetOptions{ + Atomic: true, + OldValue: oldJSONBytes, + }).Return(false, nil).Times(3) + api.On("KVSetWithOptions", "testNum", newJSONBytes, model.PluginKVSetOptions{ + Atomic: true, + OldValue: oldJSONBytes, + }).Return(true, nil).Once() + }, + }, + { + name: "Test SetAtomicWithRetries failure on get", + key: "testNum", + valueFunc: func(t *testing.T) func(old []byte) (interface{}, error) { + return func(old []byte) (interface{}, error) { + return nil, errors.New("should not have got here") + } + }, + setupAPI: func(api *plugintest.API) { + api.On("KVGet", "testNum").Return(nil, newAppError()).Once() + }, + wantErr: true, + expectedErrPrefix: "failed to get value for key testNum", + }, + { + name: "Test SetAtomicWithRetries failure on valueFunc", + key: "testNum", + valueFunc: func(t *testing.T) func(old []byte) (interface{}, error) { + return func(old []byte) (interface{}, error) { + return nil, errors.New("some user provided error") + } + }, + setupAPI: func(api *plugintest.API) { + oldJSONBytes, _ := json.Marshal(1) + api.On("KVGet", "testNum").Return(oldJSONBytes, nil).Once() + }, + wantErr: true, + expectedErrPrefix: "valueFunc failed: some user provided error", + }, + { + name: "Test SetAtomicWithRetries DB failure on set", + key: "testNum", + valueFunc: func(t *testing.T) func(old []byte) (interface{}, error) { + return func(old []byte) (interface{}, error) { + return 2, nil + } + }, + setupAPI: func(api *plugintest.API) { + oldJSONBytes, _ := json.Marshal(1) + newJSONBytes, _ := json.Marshal(2) + api.On("KVGet", "testNum").Return(oldJSONBytes, nil).Once() + api.On("KVSetWithOptions", "testNum", newJSONBytes, model.PluginKVSetOptions{ + Atomic: true, + OldValue: oldJSONBytes, + }).Return(false, newAppError()).Once() + }, + wantErr: true, + expectedErrPrefix: "DB failed to set value for key testNum", + }, + { + name: "Test SetAtomicWithRetries failure on five set attempts -- depends on numRetries constant being = 5", + key: "testNum", + valueFunc: func(t *testing.T) func(old []byte) (interface{}, error) { + return func(old []byte) (interface{}, error) { + return 2, nil + } + }, + setupAPI: func(api *plugintest.API) { + oldJSONBytes, _ := json.Marshal(1) + newJSONBytes, _ := json.Marshal(2) + api.On("KVGet", "testNum").Return(oldJSONBytes, nil).Times(5) + api.On("KVSetWithOptions", "testNum", newJSONBytes, model.PluginKVSetOptions{ + Atomic: true, + OldValue: oldJSONBytes, + }).Return(false, nil).Times(5) + }, + wantErr: true, + expectedErrPrefix: "failed to set value after 5 retries", + }, + { + name: "Test SetAtomicWithRetries success after five set attempts -- depends on numRetries constant being = 5", + key: "testNum", + valueFunc: func(t *testing.T) func(old []byte) (interface{}, error) { + return func(old []byte) (interface{}, error) { + fromDB, err := strconv.Atoi(string(old)) + if err != nil { + return nil, err + } + return fromDB + 1, nil + } + }, + setupAPI: func(api *plugintest.API) { + i1, _ := json.Marshal(1) + i2, _ := json.Marshal(2) + i3, _ := json.Marshal(3) + i4, _ := json.Marshal(4) + i5, _ := json.Marshal(5) + i6, _ := json.Marshal(6) + api.On("KVGet", "testNum").Return(i1, nil).Once() + api.On("KVSetWithOptions", "testNum", i2, model.PluginKVSetOptions{ + Atomic: true, + OldValue: i1, + }).Return(false, nil).Once() + api.On("KVGet", "testNum").Return(i2, nil).Once() + api.On("KVSetWithOptions", "testNum", i3, model.PluginKVSetOptions{ + Atomic: true, + OldValue: i2, + }).Return(false, nil).Once() + api.On("KVGet", "testNum").Return(i3, nil).Once() + api.On("KVSetWithOptions", "testNum", i4, model.PluginKVSetOptions{ + Atomic: true, + OldValue: i3, + }).Return(false, nil).Once() + api.On("KVGet", "testNum").Return(i4, nil).Once() + api.On("KVSetWithOptions", "testNum", i5, model.PluginKVSetOptions{ + Atomic: true, + OldValue: i4, + }).Return(false, nil).Once() + api.On("KVGet", "testNum").Return(i5, nil).Once() + api.On("KVSetWithOptions", "testNum", i6, model.PluginKVSetOptions{ + Atomic: true, + OldValue: i5, + }).Return(true, nil).Once() + }, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + api := &plugintest.API{} + defer api.AssertExpectations(t) + client := pluginapi.NewClient(api, &plugintest.Driver{}) + + tt.setupAPI(api) + + err := client.KV.SetAtomicWithRetries(tt.key, tt.valueFunc(t)) + if tt.wantErr { + if err == nil { + t.Errorf("SetAtomicWithRetries() error = %v, wantErr %v", err, tt.wantErr) + } + if !strings.HasPrefix(err.Error(), tt.expectedErrPrefix) { + t.Errorf("SetAtomicWithRetries() error = %s, expected prefix = %s", err, tt.expectedErrPrefix) + } + } + }) + } +} + +func TestGet(t *testing.T) { + api := &plugintest.API{} + defer api.AssertExpectations(t) + client := pluginapi.NewClient(api, &plugintest.Driver{}) + + aStringJSON, _ := json.Marshal("2") + + api.On("KVGet", "1").Return(aStringJSON, nil) + + var out string + err := client.KV.Get("1", &out) + require.NoError(t, err) + assert.Equal(t, "2", out) +} + +func TestGetNilKey(t *testing.T) { + api := &plugintest.API{} + defer api.AssertExpectations(t) + client := pluginapi.NewClient(api, &plugintest.Driver{}) + + api.On("KVGet", "1").Return(nil, nil) + + var out string + err := client.KV.Get("1", &out) + require.NoError(t, err) + assert.Empty(t, out) +} + +func TestGetInBytes(t *testing.T) { + api := &plugintest.API{} + defer api.AssertExpectations(t) + client := pluginapi.NewClient(api, &plugintest.Driver{}) + + api.On("KVGet", "1").Return([]byte{2}, nil) + + var out []byte + err := client.KV.Get("1", &out) + require.NoError(t, err) + assert.Equal(t, []byte{2}, out) + api.AssertExpectations(t) +} + +func TestDelete(t *testing.T) { + api := &plugintest.API{} + defer api.AssertExpectations(t) + client := pluginapi.NewClient(api, &plugintest.Driver{}) + + api.On("KVSetWithOptions", "1", []byte(nil), model.PluginKVSetOptions{}).Return(true, nil) + + err := client.KV.Delete("1") + require.NoError(t, err) +} + +func TestDeleteAll(t *testing.T) { + api := &plugintest.API{} + defer api.AssertExpectations(t) + client := pluginapi.NewClient(api, &plugintest.Driver{}) + + api.On("KVDeleteAll").Return(nil) + + err := client.KV.DeleteAll() + require.NoError(t, err) +} + +func TestListKeys(t *testing.T) { + t.Run("No keys", func(t *testing.T) { + api := &plugintest.API{} + defer api.AssertExpectations(t) + client := pluginapi.NewClient(api, &plugintest.Driver{}) + + api.On("KVList", 0, 100).Return(nil, nil) + + keys, err := client.KV.ListKeys(0, 100) + + assert.Empty(t, keys) + assert.NoError(t, err) + }) + + t.Run("Basic Success, one page", func(t *testing.T) { + api := &plugintest.API{} + defer api.AssertExpectations(t) + client := pluginapi.NewClient(api, &plugintest.Driver{}) + + api.On("KVList", 1, 2).Return(getKeys(2), nil) + + keys, err := client.KV.ListKeys(1, 2) + require.NoError(t, err) + require.Equal(t, getKeys(2), keys) + }) + + t.Run("success, two page, filter prefix, one", func(t *testing.T) { + api := &plugintest.API{} + defer api.AssertExpectations(t) + client := pluginapi.NewClient(api, &plugintest.Driver{}) + + api.On("KVList", 0, 100).Return(getKeys(100), nil) + + keys, err := client.KV.ListKeys(0, 100, pluginapi.WithPrefix("key99")) + assert.ElementsMatch(t, keys, []string{"key99"}) + assert.NoError(t, err) + }) + + t.Run("success, two page, filter prefix, all", func(t *testing.T) { + api := &plugintest.API{} + defer api.AssertExpectations(t) + client := pluginapi.NewClient(api, &plugintest.Driver{}) + + api.On("KVList", 0, 100).Return(getKeys(100), nil) + + keys, err := client.KV.ListKeys(0, 100, pluginapi.WithPrefix("notkey")) + assert.Empty(t, keys) + assert.NoError(t, err) + }) + + t.Run("success, two page, filter prefix, none", func(t *testing.T) { + api := &plugintest.API{} + defer api.AssertExpectations(t) + client := pluginapi.NewClient(api, &plugintest.Driver{}) + + api.On("KVList", 0, 100).Return(getKeys(100), nil) + + keys, err := client.KV.ListKeys(0, 100, pluginapi.WithPrefix("key")) + assert.ElementsMatch(t, keys, getKeys(100)) + assert.NoError(t, err) + }) + + t.Run("success, two page, checker func, one", func(t *testing.T) { + api := &plugintest.API{} + defer api.AssertExpectations(t) + client := pluginapi.NewClient(api, &plugintest.Driver{}) + + api.On("KVList", 0, 100).Return(getKeys(100), nil) + + check := func(key string) (bool, error) { + if key == "key1" { + return true, nil + } + return false, nil + } + + keys, err := client.KV.ListKeys(0, 100, pluginapi.WithChecker(check)) + assert.ElementsMatch(t, keys, []string{"key1"}) + assert.NoError(t, err) + }) + + t.Run("success, two page, checker func, all", func(t *testing.T) { + api := &plugintest.API{} + defer api.AssertExpectations(t) + client := pluginapi.NewClient(api, &plugintest.Driver{}) + + api.On("KVList", 0, 100).Return(getKeys(100), nil) + + check := func(key string) (bool, error) { + return false, nil + } + + keys, err := client.KV.ListKeys(0, 100, pluginapi.WithChecker(check)) + assert.Empty(t, keys) + assert.NoError(t, err) + }) + + t.Run("success, two page, checker func, none", func(t *testing.T) { + api := &plugintest.API{} + defer api.AssertExpectations(t) + client := pluginapi.NewClient(api, &plugintest.Driver{}) + + api.On("KVList", 0, 100).Return(getKeys(100), nil) + + check := func(key string) (bool, error) { + return true, nil + } + + keys, err := client.KV.ListKeys(0, 100, pluginapi.WithChecker(check)) + assert.ElementsMatch(t, keys, getKeys(100)) + assert.NoError(t, err) + }) + + t.Run("error, checker func", func(t *testing.T) { + api := &plugintest.API{} + defer api.AssertExpectations(t) + client := pluginapi.NewClient(api, &plugintest.Driver{}) + + api.On("KVList", 0, 100).Return([]string{"key1"}, nil) + + check := func(key string) (bool, error) { + return true, &model.AppError{} + } + + keys, err := client.KV.ListKeys(0, 100, pluginapi.WithChecker(check)) + assert.Empty(t, keys) + assert.Error(t, err) + }) + + t.Run("success, filter and checker func, partial on both", func(t *testing.T) { + api := &plugintest.API{} + defer api.AssertExpectations(t) + client := pluginapi.NewClient(api, &plugintest.Driver{}) + + api.On("KVList", 0, 100).Return([]string{"key1", "key2", "notkey3", "key4", "key5"}, nil) + + check := func(key string) (bool, error) { + if key == "key1" || key == "key5" { + return false, nil + } + return true, nil + } + + keys, err := client.KV.ListKeys(0, 100, pluginapi.WithPrefix("key"), pluginapi.WithChecker(check)) + assert.ElementsMatch(t, keys, []string{"key2", "key4"}) + assert.NoError(t, err) + }) +} + +func getKeys(count int) []string { + ret := make([]string, count) + for i := 0; i < count; i++ { + ret[i] = "key" + strconv.Itoa(i) + } + return ret +} diff --git a/server/public/pluginapi/license.go b/server/public/pluginapi/license.go new file mode 100644 index 00000000000..f332280cef3 --- /dev/null +++ b/server/public/pluginapi/license.go @@ -0,0 +1,107 @@ +package pluginapi + +import ( + "github.com/mattermost/mattermost/server/public/model" +) + +const ( + e10 = "E10" + e20 = "E20" + professional = "professional" + enterprise = "enterprise" +) + +// IsEnterpriseLicensedOrDevelopment returns true when the server is licensed with any Mattermost +// Enterprise License, or has `EnableDeveloper` and `EnableTesting` configuration settings +// enabled signaling a non-production, developer mode. +func IsEnterpriseLicensedOrDevelopment(config *model.Config, license *model.License) bool { + if license != nil { + return true + } + + return IsConfiguredForDevelopment(config) +} + +// isValidSkuShortName returns whether the SKU short name is one of the known strings; +// namely: E10 or professional, or E20 or enterprise +func isValidSkuShortName(license *model.License) bool { + if license == nil { + return false + } + + switch license.SkuShortName { + case e10, e20, professional, enterprise: + return true + default: + return false + } +} + +// IsE10LicensedOrDevelopment returns true when the server is at least licensed with a legacy Mattermost +// Enterprise E10 License or a Mattermost Professional License, or has `EnableDeveloper` and +// `EnableTesting` configuration settings enabled, signaling a non-production, developer mode. +func IsE10LicensedOrDevelopment(config *model.Config, license *model.License) bool { + if license != nil && + (license.SkuShortName == e10 || license.SkuShortName == professional || + license.SkuShortName == e20 || license.SkuShortName == enterprise) { + return true + } + + if !isValidSkuShortName(license) { + // As a fallback for licenses whose SKU short name is unknown, make a best effort to try + // and use the presence of a known E10/Professional feature as a check to determine licensing. + if license != nil && + license.Features != nil && + license.Features.LDAP != nil && + *license.Features.LDAP { + return true + } + } + + return IsConfiguredForDevelopment(config) +} + +// IsE20LicensedOrDevelopment returns true when the server is licensed with a legacy Mattermost +// Enterprise E20 License or a Mattermost Enterprise License, or has `EnableDeveloper` and +// `EnableTesting` configuration settings enabled, signaling a non-production, developer mode. +func IsE20LicensedOrDevelopment(config *model.Config, license *model.License) bool { + if license != nil && (license.SkuShortName == e20 || license.SkuShortName == enterprise) { + return true + } + + if !isValidSkuShortName(license) { + // As a fallback for licenses whose SKU short name is unknown, make a best effort to try + // and use the presence of a known E20/Enterprise feature as a check to determine licensing. + if license != nil && + license.Features != nil && + license.Features.FutureFeatures != nil && + *license.Features.FutureFeatures { + return true + } + } + + return IsConfiguredForDevelopment(config) +} + +// IsConfiguredForDevelopment returns true when the server has `EnableDeveloper` and `EnableTesting` +// configuration settings enabled, signaling a non-production, developer mode. +func IsConfiguredForDevelopment(config *model.Config) bool { + if config != nil && + config.ServiceSettings.EnableTesting != nil && + *config.ServiceSettings.EnableTesting && + config.ServiceSettings.EnableDeveloper != nil && + *config.ServiceSettings.EnableDeveloper { + return true + } + + return false +} + +// IsCloud returns true when the server is on cloud, and false otherwise. +func IsCloud(license *model.License) bool { + if license == nil || license.Features == nil || license.Features.Cloud == nil { + return false + } + + return *license.Features.Cloud +} diff --git a/server/public/pluginapi/license_test.go b/server/public/pluginapi/license_test.go new file mode 100644 index 00000000000..55835e9711f --- /dev/null +++ b/server/public/pluginapi/license_test.go @@ -0,0 +1,330 @@ +package pluginapi + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/mattermost/mattermost/server/public/model" +) + +func TestIsEnterpriseLicensedOrDevelopment(t *testing.T) { + t.Run("license, no config", func(t *testing.T) { + assert.True(t, IsEnterpriseLicensedOrDevelopment(nil, &model.License{})) + }) + + t.Run("license, nil config", func(t *testing.T) { + assert.True(t, IsEnterpriseLicensedOrDevelopment( + &model.Config{ServiceSettings: model.ServiceSettings{EnableDeveloper: nil, EnableTesting: nil}}, + &model.License{}, + )) + }) + + t.Run("no license, no config", func(t *testing.T) { + assert.False(t, IsEnterpriseLicensedOrDevelopment(nil, nil)) + }) + + t.Run("no license, nil config", func(t *testing.T) { + assert.False(t, IsEnterpriseLicensedOrDevelopment( + &model.Config{ServiceSettings: model.ServiceSettings{EnableDeveloper: nil, EnableTesting: nil}}, + nil, + )) + }) + + t.Run("no license, only developer mode", func(t *testing.T) { + assert.False(t, IsEnterpriseLicensedOrDevelopment( + &model.Config{ServiceSettings: model.ServiceSettings{EnableDeveloper: bToP(true), EnableTesting: bToP(false)}}, + nil, + )) + }) + + t.Run("no license, only testing mode", func(t *testing.T) { + assert.False(t, IsEnterpriseLicensedOrDevelopment( + &model.Config{ServiceSettings: model.ServiceSettings{EnableDeveloper: bToP(false), EnableTesting: bToP(true)}}, + nil, + )) + }) + + t.Run("no license, developer and testing mode", func(t *testing.T) { + assert.True(t, IsEnterpriseLicensedOrDevelopment( + &model.Config{ServiceSettings: model.ServiceSettings{EnableDeveloper: bToP(true), EnableTesting: bToP(true)}}, + nil, + )) + }) +} + +func TestIsE20LicensedOrDevelopment(t *testing.T) { + t.Run("nil license features", func(t *testing.T) { + assert.False(t, IsE20LicensedOrDevelopment(nil, &model.License{})) + }) + + t.Run("nil future features", func(t *testing.T) { + assert.False(t, IsE20LicensedOrDevelopment(nil, &model.License{Features: &model.Features{}})) + }) + + t.Run("disabled future features", func(t *testing.T) { + assert.False(t, IsE20LicensedOrDevelopment(nil, &model.License{Features: &model.Features{ + FutureFeatures: bToP(false), + }})) + }) + + t.Run("enabled future features", func(t *testing.T) { + assert.True(t, IsE20LicensedOrDevelopment(nil, &model.License{Features: &model.Features{ + FutureFeatures: bToP(true), + }})) + }) + + t.Run("no license, no config", func(t *testing.T) { + assert.False(t, IsE20LicensedOrDevelopment(nil, nil)) + }) + + t.Run("no license, nil config", func(t *testing.T) { + assert.False(t, IsE20LicensedOrDevelopment( + &model.Config{ServiceSettings: model.ServiceSettings{EnableDeveloper: nil, EnableTesting: nil}}, + nil, + )) + }) + + t.Run("no license, only developer mode", func(t *testing.T) { + assert.False(t, IsE20LicensedOrDevelopment( + &model.Config{ServiceSettings: model.ServiceSettings{EnableDeveloper: bToP(true), EnableTesting: bToP(false)}}, + nil, + )) + }) + + t.Run("no license, only testing mode", func(t *testing.T) { + assert.False(t, IsE20LicensedOrDevelopment( + &model.Config{ServiceSettings: model.ServiceSettings{EnableDeveloper: bToP(false), EnableTesting: bToP(true)}}, + nil, + )) + }) + + t.Run("no license, developer and testing mode", func(t *testing.T) { + assert.True(t, IsE20LicensedOrDevelopment( + &model.Config{ServiceSettings: model.ServiceSettings{EnableDeveloper: bToP(true), EnableTesting: bToP(true)}}, + nil, + )) + }) + + t.Run("license with E10 SKU name, disabled future features", func(t *testing.T) { + assert.False(t, IsE20LicensedOrDevelopment(nil, &model.License{ + SkuShortName: "E10", + Features: &model.Features{FutureFeatures: bToP(false)}, + })) + }) + + t.Run("license with E10 SKU name, enabled future features", func(t *testing.T) { + assert.False(t, IsE20LicensedOrDevelopment(nil, &model.License{ + SkuShortName: "E10", + Features: &model.Features{FutureFeatures: bToP(true)}, + })) + }) + + t.Run("license with professional SKU name, disabled future features", func(t *testing.T) { + assert.False(t, IsE20LicensedOrDevelopment(nil, &model.License{ + SkuShortName: "professional", + Features: &model.Features{FutureFeatures: bToP(false)}, + })) + }) + + t.Run("license with professional SKU name, enabled future features", func(t *testing.T) { + assert.False(t, IsE20LicensedOrDevelopment(nil, &model.License{ + SkuShortName: "professional", + Features: &model.Features{FutureFeatures: bToP(true)}, + })) + }) + t.Run("license with E20 SKU name, disabled future features", func(t *testing.T) { + assert.True(t, IsE20LicensedOrDevelopment(nil, &model.License{ + SkuShortName: "E20", + Features: &model.Features{FutureFeatures: bToP(false)}, + })) + }) + + t.Run("license with E20 SKU name, enabled future features", func(t *testing.T) { + assert.True(t, IsE20LicensedOrDevelopment(nil, &model.License{ + SkuShortName: "E20", + Features: &model.Features{FutureFeatures: bToP(true)}, + })) + }) + + t.Run("license with enterprise SKU name, disabled future features", func(t *testing.T) { + assert.True(t, IsE20LicensedOrDevelopment(nil, &model.License{ + SkuShortName: "enterprise", + Features: &model.Features{FutureFeatures: bToP(false)}, + })) + }) + + t.Run("license with enterprise SKU name, enabled future features", func(t *testing.T) { + assert.True(t, IsE20LicensedOrDevelopment(nil, &model.License{ + SkuShortName: "enterprise", + Features: &model.Features{FutureFeatures: bToP(true)}, + })) + }) + + t.Run("license with unknown SKU name, disabled future features", func(t *testing.T) { + assert.False(t, IsE20LicensedOrDevelopment(nil, &model.License{ + SkuShortName: "unknown", + Features: &model.Features{FutureFeatures: bToP(false)}, + })) + }) + + t.Run("license with unknown SKU name, enabled future features", func(t *testing.T) { + assert.True(t, IsE20LicensedOrDevelopment(nil, &model.License{ + SkuShortName: "unknown", + Features: &model.Features{FutureFeatures: bToP(true)}, + })) + }) +} + +func TestIsE10LicensedOrDevelopment(t *testing.T) { + t.Run("nil license features", func(t *testing.T) { + assert.False(t, IsE10LicensedOrDevelopment(nil, &model.License{})) + }) + + t.Run("nil future features", func(t *testing.T) { + assert.False(t, IsE10LicensedOrDevelopment(nil, &model.License{Features: &model.Features{}})) + }) + + t.Run("disabled LDAP", func(t *testing.T) { + assert.False(t, IsE10LicensedOrDevelopment(nil, &model.License{Features: &model.Features{ + LDAP: bToP(false), + }})) + }) + + t.Run("enabled LDAP", func(t *testing.T) { + assert.True(t, IsE10LicensedOrDevelopment(nil, &model.License{Features: &model.Features{ + LDAP: bToP(true), + }})) + }) + + t.Run("no license, no config", func(t *testing.T) { + assert.False(t, IsE10LicensedOrDevelopment(nil, nil)) + }) + + t.Run("no license, nil config", func(t *testing.T) { + assert.False(t, IsE10LicensedOrDevelopment( + &model.Config{ServiceSettings: model.ServiceSettings{EnableDeveloper: nil, EnableTesting: nil}}, + nil, + )) + }) + + t.Run("no license, only developer mode", func(t *testing.T) { + assert.False(t, IsE10LicensedOrDevelopment( + &model.Config{ServiceSettings: model.ServiceSettings{EnableDeveloper: bToP(true), EnableTesting: bToP(false)}}, + nil, + )) + }) + + t.Run("no license, only testing mode", func(t *testing.T) { + assert.False(t, IsE10LicensedOrDevelopment( + &model.Config{ServiceSettings: model.ServiceSettings{EnableDeveloper: bToP(false), EnableTesting: bToP(true)}}, + nil, + )) + }) + + t.Run("no license, developer and testing mode", func(t *testing.T) { + assert.True(t, IsE10LicensedOrDevelopment( + &model.Config{ServiceSettings: model.ServiceSettings{EnableDeveloper: bToP(true), EnableTesting: bToP(true)}}, + nil, + )) + }) + + t.Run("license with E10 SKU name, disabled LDAP", func(t *testing.T) { + assert.True(t, IsE10LicensedOrDevelopment(nil, &model.License{ + SkuShortName: "E10", + Features: &model.Features{LDAP: bToP(false)}, + })) + }) + + t.Run("license with E10 SKU name, enabled LDAP", func(t *testing.T) { + assert.True(t, IsE10LicensedOrDevelopment(nil, &model.License{ + SkuShortName: "E10", + Features: &model.Features{LDAP: bToP(true)}, + })) + }) + + t.Run("license with professional SKU name, disabled LDAP", func(t *testing.T) { + assert.True(t, IsE10LicensedOrDevelopment(nil, &model.License{ + SkuShortName: "professional", + Features: &model.Features{LDAP: bToP(false)}, + })) + }) + + t.Run("license with professional SKU name, enabled LDAP", func(t *testing.T) { + assert.True(t, IsE10LicensedOrDevelopment(nil, &model.License{ + SkuShortName: "professional", + Features: &model.Features{LDAP: bToP(true)}, + })) + }) + t.Run("license with E20 SKU name, disabled LDAP", func(t *testing.T) { + assert.True(t, IsE10LicensedOrDevelopment(nil, &model.License{ + SkuShortName: "E20", + Features: &model.Features{LDAP: bToP(false)}, + })) + }) + + t.Run("license with E20 SKU name, enabled LDAP", func(t *testing.T) { + assert.True(t, IsE10LicensedOrDevelopment(nil, &model.License{ + SkuShortName: "E20", + Features: &model.Features{LDAP: bToP(true)}, + })) + }) + + t.Run("license with enterprise SKU name, disabled LDAP", func(t *testing.T) { + assert.True(t, IsE10LicensedOrDevelopment(nil, &model.License{ + SkuShortName: "enterprise", + Features: &model.Features{LDAP: bToP(false)}, + })) + }) + + t.Run("license with enterprise SKU name, enabled LDAP", func(t *testing.T) { + assert.True(t, IsE10LicensedOrDevelopment(nil, &model.License{ + SkuShortName: "enterprise", + Features: &model.Features{LDAP: bToP(true)}, + })) + }) + + t.Run("license with unknown SKU name, disabled LDAP", func(t *testing.T) { + assert.False(t, IsE10LicensedOrDevelopment(nil, &model.License{ + SkuShortName: "unknown", + Features: &model.Features{LDAP: bToP(false)}, + })) + }) + + t.Run("license with unknown SKU name, enabled LDAP", func(t *testing.T) { + assert.True(t, IsE10LicensedOrDevelopment(nil, &model.License{ + SkuShortName: "unknown", + Features: &model.Features{LDAP: bToP(true)}, + })) + }) +} + +func TestIsValidSKUShortName(t *testing.T) { + t.Run("nil license", func(t *testing.T) { + assert.False(t, isValidSkuShortName(nil)) + }) + + t.Run("license with valid E10 SKU name", func(t *testing.T) { + assert.True(t, isValidSkuShortName(&model.License{SkuShortName: "E10"})) + }) + + t.Run("license with valid E20 SKU name", func(t *testing.T) { + assert.True(t, isValidSkuShortName(&model.License{SkuShortName: "E20"})) + }) + + t.Run("license with valid professional SKU name", func(t *testing.T) { + assert.True(t, isValidSkuShortName(&model.License{SkuShortName: "professional"})) + }) + + t.Run("license with valid enterprise SKU name", func(t *testing.T) { + assert.True(t, isValidSkuShortName(&model.License{SkuShortName: "enterprise"})) + }) + + t.Run("license with invalid SKU name", func(t *testing.T) { + assert.False(t, isValidSkuShortName(&model.License{SkuShortName: "invalid"})) + }) +} + +func bToP(b bool) *bool { + return &b +} diff --git a/server/public/pluginapi/log.go b/server/public/pluginapi/log.go new file mode 100644 index 00000000000..34184b66022 --- /dev/null +++ b/server/public/pluginapi/log.go @@ -0,0 +1,33 @@ +package pluginapi + +import ( + "github.com/mattermost/mattermost/server/public/plugin" +) + +// LogService exposes methods to log to the Mattermost server log. +// +// Note that standard error is automatically sent to the Mattermost server log, and standard +// output is redirected to standard error. This service enables optional structured logging. +type LogService struct { + api plugin.API +} + +// Error logs an error message, optionally structured with alternating key, value parameters. +func (l *LogService) Error(message string, keyValuePairs ...interface{}) { + l.api.LogError(message, keyValuePairs...) +} + +// Warn logs an error message, optionally structured with alternating key, value parameters. +func (l *LogService) Warn(message string, keyValuePairs ...interface{}) { + l.api.LogWarn(message, keyValuePairs...) +} + +// Info logs an error message, optionally structured with alternating key, value parameters. +func (l *LogService) Info(message string, keyValuePairs ...interface{}) { + l.api.LogInfo(message, keyValuePairs...) +} + +// Debug logs an error message, optionally structured with alternating key, value parameters. +func (l *LogService) Debug(message string, keyValuePairs ...interface{}) { + l.api.LogDebug(message, keyValuePairs...) +} diff --git a/server/public/pluginapi/logrus.go b/server/public/pluginapi/logrus.go new file mode 100644 index 00000000000..37999d012f2 --- /dev/null +++ b/server/public/pluginapi/logrus.go @@ -0,0 +1,69 @@ +package pluginapi + +import ( + "fmt" + "io" + + "github.com/sirupsen/logrus" +) + +// LogrusHook is a logrus.Hook for emitting plugin logs through the RPC API for inclusion in the +// server logs. +// +// To configure the default Logrus logger for use with plugin logging, simply invoke: +// +// pluginapi.ConfigureLogrus(logrus.StandardLogger(), pluginAPIClient) +// +// Alternatively, construct your own logger to pass to pluginapi.ConfigureLogrus. +type LogrusHook struct { + log LogService +} + +// NewLogrusHook creates a new instance of LogrusHook. +func NewLogrusHook(log LogService) *LogrusHook { + return &LogrusHook{ + log: log, + } +} + +// Levels allows LogrusHook to process any log level. +func (lh *LogrusHook) Levels() []logrus.Level { + return logrus.AllLevels +} + +// Fire proxies logrus entries through the plugin API at the appropriate level. +func (lh *LogrusHook) Fire(entry *logrus.Entry) error { + fields := []interface{}{} + for key, value := range entry.Data { + fields = append(fields, key, fmt.Sprintf("%+v", value)) + } + + if entry.Caller != nil { + fields = append(fields, "plugin_caller", fmt.Sprintf("%s:%d", entry.Caller.File, entry.Caller.Line)) + } + + switch entry.Level { + case logrus.PanicLevel, logrus.FatalLevel, logrus.ErrorLevel: + lh.log.Error(entry.Message, fields...) + case logrus.WarnLevel: + lh.log.Warn(entry.Message, fields...) + case logrus.InfoLevel: + lh.log.Info(entry.Message, fields...) + case logrus.DebugLevel, logrus.TraceLevel: + lh.log.Debug(entry.Message, fields...) + } + + return nil +} + +// ConfigureLogrus configures the given logrus logger with a hook to proxy through the RPC API, +// discarding the default output to avoid duplicating the events across the standard STDOUT proxy. +func ConfigureLogrus(logger *logrus.Logger, client *Client) { + hook := NewLogrusHook(client.Log) + logger.Hooks.Add(hook) + logger.SetOutput(io.Discard) + logrus.SetReportCaller(true) + + // By default, log everything to the server, and let it decide what gets through. + logrus.SetLevel(logrus.TraceLevel) +} diff --git a/server/public/pluginapi/logrus_test.go b/server/public/pluginapi/logrus_test.go new file mode 100644 index 00000000000..04c5481a580 --- /dev/null +++ b/server/public/pluginapi/logrus_test.go @@ -0,0 +1,85 @@ +package pluginapi_test + +import ( + "testing" + + "github.com/mattermost/mattermost/server/public/plugin/plugintest" + "github.com/sirupsen/logrus" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + + "github.com/mattermost/mattermost/server/public/pluginapi" +) + +func TestLogrus(t *testing.T) { + testCases := []struct { + Level logrus.Level + APICall string + }{ + {logrus.PanicLevel, "LogError"}, + {logrus.FatalLevel, "LogError"}, + {logrus.ErrorLevel, "LogError"}, + {logrus.WarnLevel, "LogWarn"}, + {logrus.InfoLevel, "LogInfo"}, + {logrus.DebugLevel, "LogDebug"}, + {logrus.TraceLevel, "LogDebug"}, + } + + for _, testCase := range testCases { + t.Run(testCase.Level.String(), func(t *testing.T) { + logger := logrus.New() + logger.SetLevel(logrus.TraceLevel) // not testing logrus filtering + logger.ReportCaller = true + + api := &plugintest.API{} + defer api.AssertExpectations(t) + client := pluginapi.NewClient(api, &plugintest.Driver{}) + + pluginapi.ConfigureLogrus(logger, client) + + // Parameter order of map is non-deterministic, so expect either. + api.On(testCase.APICall, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything) + + entry := logger.WithFields(logrus.Fields{ + "a": "a", + "b": 1, + }) + + if testCase.Level == logrus.PanicLevel { + done := make(chan bool) + go func() { + defer func() { + r := recover() + assert.NotNil(t, r, "expected panic") + close(done) + }() + + entry.Panic("message") + }() + <-done + } else { + entry.Log(testCase.Level, "message") + } + + // Assert the required API call was executed at most once. + if api.AssertNumberOfCalls(t, testCase.APICall, 1) { + call := api.Calls[0] + for i := 1; i < len(call.Arguments)-1; i += 2 { + argument := call.Arguments[i] + value := call.Arguments[i+1] + + switch argument { + case "a": + assert.Equal(t, "a", value, "unexpected value for a") + case "b": + assert.Equal(t, "1", value, "unexpected value for b") + case "plugin_caller": + assert.IsType(t, "string", value) + default: + assert.Fail(t, "unexpected argument and value", "%v: %v", argument, value) + } + } + } + }) + } +} diff --git a/server/public/pluginapi/oauth.go b/server/public/pluginapi/oauth.go new file mode 100644 index 00000000000..ef3dd8ee879 --- /dev/null +++ b/server/public/pluginapi/oauth.go @@ -0,0 +1,55 @@ +package pluginapi + +import ( + "github.com/mattermost/mattermost/server/public/model" + "github.com/mattermost/mattermost/server/public/plugin" +) + +// UserService exposes methods to manipulate OAuth Apps. +type OAuthService struct { + api plugin.API +} + +// Create creates a new OAuth App. +// +// Minimum server version: 5.38 +func (o *OAuthService) Create(app *model.OAuthApp) error { + createdApp, appErr := o.api.CreateOAuthApp(app) + if appErr != nil { + return normalizeAppErr(appErr) + } + + *app = *createdApp + + return nil +} + +// Get gets an existing OAuth App by id. +// +// Minimum server version: 5.38 +func (o *OAuthService) Get(appID string) (*model.OAuthApp, error) { + app, appErr := o.api.GetOAuthApp(appID) + + return app, normalizeAppErr(appErr) +} + +// Update updates an existing OAuth App. +// +// Minimum server version: 5.38 +func (o *OAuthService) Update(app *model.OAuthApp) error { + updatedApp, appErr := o.api.UpdateOAuthApp(app) + if appErr != nil { + return normalizeAppErr(appErr) + } + + *app = *updatedApp + + return nil +} + +// Delete deletes an existing OAuth App by id. +// +// Minimum server version: 5.38 +func (o *OAuthService) Delete(appID string) error { + return normalizeAppErr(o.api.DeleteOAuthApp(appID)) +} diff --git a/server/public/pluginapi/plugin_test.go b/server/public/pluginapi/plugin_test.go new file mode 100644 index 00000000000..7fe76a8a222 --- /dev/null +++ b/server/public/pluginapi/plugin_test.go @@ -0,0 +1,170 @@ +package pluginapi_test + +import ( + "net/http" + "net/http/httptest" + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/mattermost/mattermost/server/public/model" + "github.com/mattermost/mattermost/server/public/plugin/plugintest" + "github.com/mattermost/mattermost/server/public/plugin/plugintest/mock" + "github.com/mattermost/mattermost/server/public/pluginapi" +) + +func TestInstallPluginFromURL(t *testing.T) { + replace := true + + t.Run("incompatible server version", func(t *testing.T) { + api := &plugintest.API{} + api.On("GetServerVersion").Return("5.1.0") + client := pluginapi.NewClient(api, &plugintest.Driver{}) + + _, err := client.Plugin.InstallPluginFromURL("", true) + + assert.Error(t, err) + assert.Equal(t, "incompatible server version for plugin, minimum required version: 5.18.0, current version: 5.1.0", err.Error()) + }) + + t.Run("error while parsing the download url", func(t *testing.T) { + api := &plugintest.API{} + api.On("GetServerVersion").Return("5.19.0") + client := pluginapi.NewClient(api, &plugintest.Driver{}) + + _, err := client.Plugin.InstallPluginFromURL("http://%41:8080/", replace) + + assert.Error(t, err) + assert.Equal(t, "error while parsing url: parse \"http://%41:8080/\": invalid URL escape \"%41\"", err.Error()) + }) + + t.Run("errors out while downloading file", func(t *testing.T) { + api := &plugintest.API{} + api.On("GetServerVersion").Return("5.19.0") + client := pluginapi.NewClient(api, &plugintest.Driver{}) + + testServer := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) { + res.WriteHeader(http.StatusInternalServerError) + })) + defer testServer.Close() + url := testServer.URL + + _, err := client.Plugin.InstallPluginFromURL(url, replace) + + assert.Error(t, err) + assert.Equal(t, "received 500 status code while downloading plugin from server", err.Error()) + }) + + t.Run("downloads the file successfully", func(t *testing.T) { + api := &plugintest.API{} + api.On("GetServerVersion").Return("5.19.0") + client := pluginapi.NewClient(api, &plugintest.Driver{}) + + tarData, err := os.ReadFile(filepath.Join("../../tests", "testplugin.tar.gz")) + require.NoError(t, err) + expectedManifest := &model.Manifest{Id: "testplugin"} + api.On("InstallPlugin", mock.Anything, false).Return(expectedManifest, nil) + + testServer := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) { + res.WriteHeader(http.StatusOK) + _, _ = res.Write(tarData) + })) + defer testServer.Close() + url := testServer.URL + + manifest, err := client.Plugin.InstallPluginFromURL(url, false) + + assert.NoError(t, err) + assert.Equal(t, "testplugin", manifest.Id) + }) + + t.Run("the url pointing to server is incorrect", func(t *testing.T) { + api := &plugintest.API{} + api.On("GetServerVersion").Return("5.19.0") + client := pluginapi.NewClient(api, &plugintest.Driver{}) + testServer := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) { + res.WriteHeader(http.StatusNotFound) + })) + defer testServer.Close() + url := testServer.URL + + _, err := client.Plugin.InstallPluginFromURL(url, false) + + assert.Error(t, err) + assert.Equal(t, "received 404 status code while downloading plugin from server", err.Error()) + }) +} + +func TestGetPluginAssetURL(t *testing.T) { + siteURL := "https://mattermost.example.com" + api := &plugintest.API{} + api.On("GetConfig").Return(&model.Config{ServiceSettings: model.ServiceSettings{SiteURL: &siteURL}}) + + client := pluginapi.NewClient(api, &plugintest.Driver{}) + + t.Run("Valid asset directory was provided", func(t *testing.T) { + pluginID := "mattermost-1234" + dir := "assets" + wantedURL := "https://mattermost.example.com/mattermost-1234/assets" + gotURL, err := client.System.GetPluginAssetURL(pluginID, dir) + + assert.Equalf(t, wantedURL, gotURL, "GetPluginAssetURL(%q, %q) got=%q; want=%v", pluginID, dir, gotURL, wantedURL) + assert.NoError(t, err) + }) + + t.Run("Valid asset directory path was provided", func(t *testing.T) { + pluginID := "mattermost-1234" + dirPath := "/mattermost/assets" + wantedURL := "https://mattermost.example.com/mattermost-1234/mattermost/assets" + gotURL, err := client.System.GetPluginAssetURL(pluginID, dirPath) + + assert.Equalf(t, wantedURL, gotURL, "GetPluginAssetURL(%q, %q) got=%q; want=%q", pluginID, dirPath, gotURL, wantedURL) + assert.NoError(t, err) + }) + + t.Run("Valid pluginID was provided", func(t *testing.T) { + pluginID := "mattermost-1234" + dir := "assets" + wantedURL := "https://mattermost.example.com/mattermost-1234/assets" + gotURL, err := client.System.GetPluginAssetURL(pluginID, dir) + + assert.Equalf(t, wantedURL, gotURL, "GetPluginAssetURL(%q, %q) got=%q; want=%q", pluginID, dir, gotURL, wantedURL) + assert.NoError(t, err) + }) + + t.Run("Invalid asset directory name was provided", func(t *testing.T) { + pluginID := "mattermost-1234" + dir := "" + want := "" + gotURL, err := client.System.GetPluginAssetURL(pluginID, dir) + + assert.Emptyf(t, gotURL, "GetPluginAssetURL(%q, %q) got=%s; want=%q", pluginID, dir, gotURL, want) + assert.Error(t, err) + }) + + t.Run("Invalid pluginID was provided", func(t *testing.T) { + pluginID := "" + dir := "assets" + want := "" + gotURL, err := client.System.GetPluginAssetURL(pluginID, dir) + + assert.Emptyf(t, gotURL, "GetPluginAssetURL(%q, %q) got=%q; want=%q", pluginID, dir, gotURL, want) + assert.Error(t, err) + }) + + siteURL = "" + api.On("GetConfig").Return(&model.Config{ServiceSettings: model.ServiceSettings{SiteURL: &siteURL}}) + + t.Run("Empty SiteURL was configured", func(t *testing.T) { + pluginID := "mattermost-1234" + dir := "assets" + want := "" + gotURL, err := client.System.GetPluginAssetURL(pluginID, dir) + + assert.Emptyf(t, gotURL, "GetPluginAssetURL(%q, %q) got=%q; want=%q", pluginID, dir, gotURL, want) + assert.Error(t, err) + }) +} diff --git a/server/public/pluginapi/plugins.go b/server/public/pluginapi/plugins.go new file mode 100644 index 00000000000..babe7d2b233 --- /dev/null +++ b/server/public/pluginapi/plugins.go @@ -0,0 +1,114 @@ +package pluginapi + +import ( + "io" + "net/http" + "net/url" + "time" + + "github.com/pkg/errors" + + "github.com/mattermost/mattermost/server/public/model" + "github.com/mattermost/mattermost/server/public/plugin" +) + +// PluginService exposes methods to manipulate the set of plugins as well as communicate with +// other plugin instances. +type PluginService struct { + api plugin.API +} + +// List will return a list of plugin manifests for currently active plugins. +// +// Minimum server version: 5.6 +func (p *PluginService) List() ([]*model.Manifest, error) { + manifests, appErr := p.api.GetPlugins() + + return manifests, normalizeAppErr(appErr) +} + +// Install will upload another plugin with tar.gz file. +// Previous version will be replaced on replace true. +// +// Minimum server version: 5.18 +func (p *PluginService) Install(file io.Reader, replace bool) (*model.Manifest, error) { + manifest, appErr := p.api.InstallPlugin(file, replace) + + return manifest, normalizeAppErr(appErr) +} + +// InstallPluginFromURL installs the plugin from the provided url. +// +// Minimum server version: 5.18 +func (p *PluginService) InstallPluginFromURL(downloadURL string, replace bool) (*model.Manifest, error) { + err := ensureServerVersion(p.api, "5.18.0") + if err != nil { + return nil, err + } + + parsedURL, err := url.Parse(downloadURL) + if err != nil { + return nil, errors.Wrap(err, "error while parsing url") + } + + client := &http.Client{Timeout: time.Hour} + response, err := client.Get(parsedURL.String()) + if err != nil { + return nil, errors.Wrap(err, "unable to download the plugin") + } + defer response.Body.Close() + + if response.StatusCode != http.StatusOK { + return nil, errors.Errorf("received %d status code while downloading plugin from server", response.StatusCode) + } + + manifest, err := p.Install(response.Body, replace) + if err != nil { + return nil, errors.Wrap(err, "unable to install plugin on server") + } + + return manifest, nil +} + +// Enable will enable an plugin installed. +// +// Minimum server version: 5.6 +func (p *PluginService) Enable(id string) error { + appErr := p.api.EnablePlugin(id) + + return normalizeAppErr(appErr) +} + +// Disable will disable an enabled plugin. +// +// Minimum server version: 5.6 +func (p *PluginService) Disable(id string) error { + appErr := p.api.DisablePlugin(id) + + return normalizeAppErr(appErr) +} + +// Remove will disable and delete a plugin. +// +// Minimum server version: 5.6 +func (p *PluginService) Remove(id string) error { + appErr := p.api.RemovePlugin(id) + + return normalizeAppErr(appErr) +} + +// GetPluginStatus will return the status of a plugin. +// +// Minimum server version: 5.6 +func (p *PluginService) GetPluginStatus(id string) (*model.PluginStatus, error) { + pluginStatus, appErr := p.api.GetPluginStatus(id) + + return pluginStatus, normalizeAppErr(appErr) +} + +// HTTP allows inter-plugin requests to plugin APIs. +// +// Minimum server version: 5.18 +func (p *PluginService) HTTP(request *http.Request) *http.Response { + return p.api.PluginHTTP(request) +} diff --git a/server/public/pluginapi/post.go b/server/public/pluginapi/post.go new file mode 100644 index 00000000000..e4328fe4dd1 --- /dev/null +++ b/server/public/pluginapi/post.go @@ -0,0 +1,335 @@ +package pluginapi + +import ( + "github.com/pkg/errors" + + "github.com/mattermost/mattermost/server/public/model" + "github.com/mattermost/mattermost/server/public/plugin" +) + +// PostService exposes methods to manipulate posts. +type PostService struct { + api plugin.API +} + +// CreatePost creates a post. +// +// Minimum server version: 5.2 +func (p *PostService) CreatePost(post *model.Post) error { + createdPost, appErr := p.api.CreatePost(post) + if appErr != nil { + return normalizeAppErr(appErr) + } + + err := createdPost.ShallowCopy(post) + if err != nil { + return err + } + + return nil +} + +// DM sends a post as a direct message +// +// Minimum server version: 5.2 +func (p *PostService) DM(senderUserID, receiverUserID string, post *model.Post) error { + channel, appErr := p.api.GetDirectChannel(senderUserID, receiverUserID) + if appErr != nil { + return normalizeAppErr(appErr) + } + post.ChannelId = channel.Id + post.UserId = senderUserID + return p.CreatePost(post) +} + +// GetPost gets a post. +// +// Minimum server version: 5.2 +func (p *PostService) GetPost(postID string) (*model.Post, error) { + post, appErr := p.api.GetPost(postID) + + return post, normalizeAppErr(appErr) +} + +// UpdatePost updates a post. +// +// Minimum server version: 5.2 +func (p *PostService) UpdatePost(post *model.Post) error { + updatedPost, appErr := p.api.UpdatePost(post) + if appErr != nil { + return normalizeAppErr(appErr) + } + + err := updatedPost.ShallowCopy(post) + if err != nil { + return err + } + + return nil +} + +// DeletePost deletes a post. +// +// Minimum server version: 5.2 +func (p *PostService) DeletePost(postID string) error { + return normalizeAppErr(p.api.DeletePost(postID)) +} + +// SendEphemeralPost creates an ephemeral post. +// +// Minimum server version: 5.2 +func (p *PostService) SendEphemeralPost(userID string, post *model.Post) { + *post = *p.api.SendEphemeralPost(userID, post) +} + +// UpdateEphemeralPost updates an ephemeral message previously sent to the user. +// EXPERIMENTAL: This API is experimental and can be changed without advance notice. +// +// Minimum server version: 5.2 +func (p *PostService) UpdateEphemeralPost(userID string, post *model.Post) { + *post = *p.api.UpdateEphemeralPost(userID, post) +} + +// DeleteEphemeralPost deletes an ephemeral message previously sent to the user. +// EXPERIMENTAL: This API is experimental and can be changed without advance notice. +// +// Minimum server version: 5.2 +func (p *PostService) DeleteEphemeralPost(userID, postID string) { + p.api.DeleteEphemeralPost(userID, postID) +} + +// GetPostThread gets a post with all the other posts in the same thread. +// +// Minimum server version: 5.6 +func (p *PostService) GetPostThread(postID string) (*model.PostList, error) { + postList, appErr := p.api.GetPostThread(postID) + + return postList, normalizeAppErr(appErr) +} + +// GetPostsSince gets posts created after a specified time as Unix time in milliseconds. +// +// Minimum server version: 5.6 +func (p *PostService) GetPostsSince(channelID string, time int64) (*model.PostList, error) { + postList, appErr := p.api.GetPostsSince(channelID, time) + + return postList, normalizeAppErr(appErr) +} + +// GetPostsAfter gets a page of posts that were posted after the post provided. +// +// Minimum server version: 5.6 +func (p *PostService) GetPostsAfter(channelID, postID string, page, perPage int) (*model.PostList, error) { + postList, appErr := p.api.GetPostsAfter(channelID, postID, page, perPage) + + return postList, normalizeAppErr(appErr) +} + +// GetPostsBefore gets a page of posts that were posted before the post provided. +// +// Minimum server version: 5.6 +func (p *PostService) GetPostsBefore(channelID, postID string, page, perPage int) (*model.PostList, error) { + postList, appErr := p.api.GetPostsBefore(channelID, postID, page, perPage) + + return postList, normalizeAppErr(appErr) +} + +// GetPostsForChannel gets a list of posts for a channel. +// +// Minimum server version: 5.6 +func (p *PostService) GetPostsForChannel(channelID string, page, perPage int) (*model.PostList, error) { + postList, appErr := p.api.GetPostsForChannel(channelID, page, perPage) + + return postList, normalizeAppErr(appErr) +} + +// SearchPostsInTeam returns a list of posts in a specific team that match the given params. +// +// Minimum server version: 5.10 +func (p *PostService) SearchPostsInTeam(teamID string, paramsList []*model.SearchParams) ([]*model.Post, error) { + postList, appErr := p.api.SearchPostsInTeam(teamID, paramsList) + + return postList, normalizeAppErr(appErr) +} + +// AddReaction add a reaction to a post. +// +// Minimum server version: 5.3 +func (p *PostService) AddReaction(reaction *model.Reaction) error { + addedReaction, appErr := p.api.AddReaction(reaction) + if appErr != nil { + return normalizeAppErr(appErr) + } + + *reaction = *addedReaction + + return nil +} + +// GetReactions get the reactions of a post. +// +// Minimum server version: 5.3 +func (p *PostService) GetReactions(postID string) ([]*model.Reaction, error) { + reactions, appErr := p.api.GetReactions(postID) + + return reactions, normalizeAppErr(appErr) +} + +// RemoveReaction remove a reaction from a post. +// +// Minimum server version: 5.3 +func (p *PostService) RemoveReaction(reaction *model.Reaction) error { + return normalizeAppErr(p.api.RemoveReaction(reaction)) +} + +type ShouldProcessMessageOption func(*shouldProcessMessageOptions) + +type shouldProcessMessageOptions struct { + AllowSystemMessages bool + AllowBots bool + AllowWebhook bool + FilterChannelIDs []string + FilterUserIDs []string + OnlyBotDMs bool + BotID string +} + +// AllowSystemMessages configures a call to ShouldProcessMessage to return true for system messages. +// +// As it is typically desirable only to consume messages from users of the system, ShouldProcessMessage ignores system messages by default. +func AllowSystemMessages() ShouldProcessMessageOption { + return func(options *shouldProcessMessageOptions) { + options.AllowSystemMessages = true + } +} + +// AllowBots configures a call to ShouldProcessMessage to return true for bot posts. +// +// As it is typically desirable only to consume messages from human users of the system, ShouldProcessMessage ignores bot messages by default. +// When allowing bots, take care to avoid a loop where two plugins respond to each others posts repeatedly. +func AllowBots() ShouldProcessMessageOption { + return func(options *shouldProcessMessageOptions) { + options.AllowBots = true + } +} + +// AllowWebhook configures a call to ShouldProcessMessage to return true for posts from webhook. +// +// As it is typically desirable only to consume messages from human users of the system, ShouldProcessMessage ignores webhook messages by default. +func AllowWebhook() ShouldProcessMessageOption { + return func(options *shouldProcessMessageOptions) { + options.AllowWebhook = true + } +} + +// FilterChannelIDs configures a call to ShouldProcessMessage to return true only for the given channels. +// +// By default, posts from all channels are allowed to be processed. +func FilterChannelIDs(filterChannelIDs []string) ShouldProcessMessageOption { + return func(options *shouldProcessMessageOptions) { + options.FilterChannelIDs = filterChannelIDs + } +} + +// FilterUserIDs configures a call to ShouldProcessMessage to return true only for the given users. +// +// By default, posts from all non-bot users are allowed. +func FilterUserIDs(filterUserIDs []string) ShouldProcessMessageOption { + return func(options *shouldProcessMessageOptions) { + options.FilterUserIDs = filterUserIDs + } +} + +// OnlyBotDMs configures a call to ShouldProcessMessage to return true only for direct messages sent to the bot created by EnsureBot. +// +// By default, posts from all channels are allowed. +func OnlyBotDMs() ShouldProcessMessageOption { + return func(options *shouldProcessMessageOptions) { + options.OnlyBotDMs = true + } +} + +// If provided, BotID configures ShouldProcessMessage to skip its retrieval from the store. +// +// By default, posts from all non-bot users are allowed. +func BotID(botID string) ShouldProcessMessageOption { + return func(options *shouldProcessMessageOptions) { + options.BotID = botID + } +} + +// ShouldProcessMessage returns if the message should be processed by a message hook. +// +// Use this method to avoid processing unnecessary messages in a MessageHasBeenPosted +// or MessageWillBePosted hook, and indeed in some cases avoid an infinite loop between +// two automated bots or plugins. +// +// The behavior is customizable using the given options, since plugin needs may vary. +// By default, system messages and messages from bots will be skipped. +// +// Minimum server version: 5.2 +func (p *PostService) ShouldProcessMessage(post *model.Post, options ...ShouldProcessMessageOption) (bool, error) { + messageProcessOptions := &shouldProcessMessageOptions{} + for _, option := range options { + option(messageProcessOptions) + } + + var botIDBytes []byte + var kvGetErr *model.AppError + + if messageProcessOptions.BotID != "" { + botIDBytes = []byte(messageProcessOptions.BotID) + } else { + botIDBytes, kvGetErr = p.api.KVGet(botUserKey) + + if kvGetErr != nil { + return false, errors.Wrap(kvGetErr, "failed to get bot") + } + } + + if botIDBytes != nil { + if post.UserId == string(botIDBytes) { + return false, nil + } + } + + if post.IsSystemMessage() && !messageProcessOptions.AllowSystemMessages { + return false, nil + } + + if !messageProcessOptions.AllowWebhook && post.GetProp("from_webhook") == "true" { + return false, nil + } + + if !messageProcessOptions.AllowBots { + user, appErr := p.api.GetUser(post.UserId) + if appErr != nil { + return false, errors.Wrap(appErr, "unable to get user") + } + + if user.IsBot { + return false, nil + } + } + + if len(messageProcessOptions.FilterChannelIDs) != 0 && !stringInSlice(post.ChannelId, messageProcessOptions.FilterChannelIDs) { + return false, nil + } + + if len(messageProcessOptions.FilterUserIDs) != 0 && !stringInSlice(post.UserId, messageProcessOptions.FilterUserIDs) { + return false, nil + } + + if botIDBytes != nil && messageProcessOptions.OnlyBotDMs { + channel, appErr := p.api.GetChannel(post.ChannelId) + if appErr != nil { + return false, errors.Wrap(appErr, "unable to get channel") + } + + if !model.IsBotDMChannel(channel, string(botIDBytes)) { + return false, nil + } + } + + return true, nil +} diff --git a/server/public/pluginapi/post_test.go b/server/public/pluginapi/post_test.go new file mode 100644 index 00000000000..8c9aa0ff3dd --- /dev/null +++ b/server/public/pluginapi/post_test.go @@ -0,0 +1,741 @@ +package pluginapi_test + +import ( + "net/http" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/mattermost/mattermost/server/public/model" + "github.com/mattermost/mattermost/server/public/plugin" + "github.com/mattermost/mattermost/server/public/plugin/plugintest" + "github.com/mattermost/mattermost/server/public/pluginapi" +) + +func TestCreatePost(t *testing.T) { + t.Run("success", func(t *testing.T) { + api := &plugintest.API{} + defer api.AssertExpectations(t) + client := pluginapi.NewClient(api, &plugintest.Driver{}) + + id := model.NewId() + in := &model.Post{ + Id: "postID", + } + out := in.Clone() + out.Id = id + api.On("CreatePost", in).Return(out, nil) + + err := client.Post.CreatePost(in) + require.NoError(t, err) + assert.Equal(t, out, in) + }) + + t.Run("failure", func(t *testing.T) { + api := &plugintest.API{} + defer api.AssertExpectations(t) + client := pluginapi.NewClient(api, &plugintest.Driver{}) + + in := &model.Post{ + Id: "postID", + } + out := in.Clone() + api.On("CreatePost", in).Return(nil, newAppError()) + + err := client.Post.CreatePost(in) + require.EqualError(t, err, "here: id, an error occurred") + assert.Equal(t, out, in) + }) +} + +func TestGetPost(t *testing.T) { + t.Run("success", func(t *testing.T) { + api := &plugintest.API{} + defer api.AssertExpectations(t) + client := pluginapi.NewClient(api, &plugintest.Driver{}) + + postID := "postID" + expectedPost := &model.Post{ + Id: postID, + } + api.On("GetPost", postID).Return(expectedPost, nil) + + actualPost, err := client.Post.GetPost(postID) + require.NoError(t, err) + assert.Equal(t, expectedPost, actualPost) + }) + + t.Run("failure", func(t *testing.T) { + api := &plugintest.API{} + defer api.AssertExpectations(t) + client := pluginapi.NewClient(api, &plugintest.Driver{}) + + postID := "postID" + api.On("GetPost", postID).Return(nil, newAppError()) + + actualPost, err := client.Post.GetPost(postID) + require.EqualError(t, err, "here: id, an error occurred") + assert.Nil(t, actualPost) + }) +} + +func TestUpdatePost(t *testing.T) { + t.Run("success", func(t *testing.T) { + api := &plugintest.API{} + defer api.AssertExpectations(t) + client := pluginapi.NewClient(api, &plugintest.Driver{}) + + now := model.GetMillis() + in := &model.Post{ + Id: "postID", + } + out := in.Clone() + out.UpdateAt = now + api.On("UpdatePost", in).Return(out, nil) + + err := client.Post.UpdatePost(in) + require.NoError(t, err) + require.Equal(t, out, in) + }) + + t.Run("failure", func(t *testing.T) { + api := &plugintest.API{} + defer api.AssertExpectations(t) + client := pluginapi.NewClient(api, &plugintest.Driver{}) + + in := &model.Post{ + Id: "postID", + } + out := in.Clone() + api.On("UpdatePost", in).Return(nil, newAppError()) + + err := client.Post.UpdatePost(in) + require.EqualError(t, err, "here: id, an error occurred") + assert.Equal(t, out, in) + }) +} + +func TestDeletePost(t *testing.T) { + t.Run("success", func(t *testing.T) { + api := &plugintest.API{} + defer api.AssertExpectations(t) + client := pluginapi.NewClient(api, &plugintest.Driver{}) + + postID := "postID" + + api.On("DeletePost", postID).Return(nil) + + err := client.Post.DeletePost(postID) + require.NoError(t, err) + }) + + t.Run("failure", func(t *testing.T) { + api := &plugintest.API{} + defer api.AssertExpectations(t) + client := pluginapi.NewClient(api, &plugintest.Driver{}) + + postID := "postID" + api.On("DeletePost", postID).Return(newAppError()) + + err := client.Post.DeletePost(postID) + require.EqualError(t, err, "here: id, an error occurred") + }) +} + +func TestSendEphemeralPost(t *testing.T) { + api := &plugintest.API{} + defer api.AssertExpectations(t) + client := pluginapi.NewClient(api, &plugintest.Driver{}) + + userID := "userID" + expectedPost := &model.Post{ + Id: "postID", + } + api.On("SendEphemeralPost", userID, expectedPost).Return(expectedPost, nil) + + client.Post.SendEphemeralPost(userID, expectedPost) +} + +func TestUpdateEphemeralPost(t *testing.T) { + api := &plugintest.API{} + defer api.AssertExpectations(t) + client := pluginapi.NewClient(api, &plugintest.Driver{}) + + now := model.GetMillis() + userID := "userID" + in := &model.Post{ + Id: "postID", + } + out := in.Clone() + out.UpdateAt = now + api.On("UpdateEphemeralPost", userID, in).Return(out, nil) + + client.Post.UpdateEphemeralPost(userID, in) + assert.Equal(t, out, in) +} + +func TestDeleteEphemeralPost(t *testing.T) { + api := &plugintest.API{} + defer api.AssertExpectations(t) + client := pluginapi.NewClient(api, &plugintest.Driver{}) + + userID := "userID" + postID := "postID" + api.On("DeleteEphemeralPost", userID, postID).Return() + + client.Post.DeleteEphemeralPost(userID, postID) +} + +func TestGetPostThread(t *testing.T) { + t.Run("success", func(t *testing.T) { + api := &plugintest.API{} + defer api.AssertExpectations(t) + client := pluginapi.NewClient(api, &plugintest.Driver{}) + + postID := "postID" + expectedPostList := model.NewPostList() + expectedPostList.AddPost(&model.Post{Id: postID}) + + api.On("GetPostThread", postID).Return(expectedPostList, nil) + + actualPostList, err := client.Post.GetPostThread(postID) + require.NoError(t, err) + assert.Equal(t, expectedPostList, actualPostList) + }) + + t.Run("failure", func(t *testing.T) { + api := &plugintest.API{} + defer api.AssertExpectations(t) + client := pluginapi.NewClient(api, &plugintest.Driver{}) + + postID := "postID" + api.On("GetPostThread", postID).Return(nil, newAppError()) + + actualPostList, err := client.Post.GetPostThread(postID) + require.EqualError(t, err, "here: id, an error occurred") + assert.Nil(t, actualPostList) + }) +} + +func TestGetPostsSince(t *testing.T) { + t.Run("success", func(t *testing.T) { + api := &plugintest.API{} + defer api.AssertExpectations(t) + client := pluginapi.NewClient(api, &plugintest.Driver{}) + + channelID := "channelID" + time := int64(0) + expectedPostList := model.NewPostList() + expectedPostList.AddPost(&model.Post{ChannelId: channelID}) + + api.On("GetPostsSince", channelID, time).Return(expectedPostList, nil) + + actualPostList, err := client.Post.GetPostsSince(channelID, time) + require.NoError(t, err) + assert.Equal(t, expectedPostList, actualPostList) + }) + + t.Run("failure", func(t *testing.T) { + api := &plugintest.API{} + defer api.AssertExpectations(t) + client := pluginapi.NewClient(api, &plugintest.Driver{}) + + channelID := "channelID" + time := int64(0) + api.On("GetPostsSince", channelID, time).Return(nil, newAppError()) + + actualPostList, err := client.Post.GetPostsSince(channelID, time) + require.EqualError(t, err, "here: id, an error occurred") + assert.Nil(t, actualPostList) + }) +} + +func TestGetPostsAfter(t *testing.T) { + t.Run("success", func(t *testing.T) { + api := &plugintest.API{} + defer api.AssertExpectations(t) + client := pluginapi.NewClient(api, &plugintest.Driver{}) + + channelID := "channelID" + postID := "postID" + expectedPostList := model.NewPostList() + expectedPostList.AddPost(&model.Post{ChannelId: channelID}) + + api.On("GetPostsAfter", channelID, postID, 0, 0).Return(expectedPostList, nil) + + actualPostList, err := client.Post.GetPostsAfter(channelID, postID, 0, 0) + require.NoError(t, err) + assert.Equal(t, expectedPostList, actualPostList) + }) + + t.Run("failure", func(t *testing.T) { + api := &plugintest.API{} + defer api.AssertExpectations(t) + client := pluginapi.NewClient(api, &plugintest.Driver{}) + + channelID := "channelID" + postID := "postID" + api.On("GetPostsAfter", channelID, postID, 0, 0).Return(nil, newAppError()) + + actualPostList, err := client.Post.GetPostsAfter(channelID, postID, 0, 0) + require.EqualError(t, err, "here: id, an error occurred") + assert.Nil(t, actualPostList) + }) +} + +func TestGetPostsBefore(t *testing.T) { + t.Run("success", func(t *testing.T) { + api := &plugintest.API{} + defer api.AssertExpectations(t) + client := pluginapi.NewClient(api, &plugintest.Driver{}) + + channelID := "channelID" + postID := "postID" + expectedPostList := model.NewPostList() + expectedPostList.AddPost(&model.Post{ChannelId: channelID}) + + api.On("GetPostsBefore", channelID, postID, 0, 0).Return(expectedPostList, nil) + + actualPostList, err := client.Post.GetPostsBefore(channelID, postID, 0, 0) + require.NoError(t, err) + assert.Equal(t, expectedPostList, actualPostList) + }) + + t.Run("failure", func(t *testing.T) { + api := &plugintest.API{} + defer api.AssertExpectations(t) + client := pluginapi.NewClient(api, &plugintest.Driver{}) + + channelID := "channelID" + postID := "postID" + api.On("GetPostsBefore", channelID, postID, 0, 0).Return(nil, newAppError()) + + actualPostList, err := client.Post.GetPostsBefore(channelID, postID, 0, 0) + require.EqualError(t, err, "here: id, an error occurred") + assert.Nil(t, actualPostList) + }) +} + +func TestGetPostsForChannel(t *testing.T) { + t.Run("success", func(t *testing.T) { + api := &plugintest.API{} + defer api.AssertExpectations(t) + client := pluginapi.NewClient(api, &plugintest.Driver{}) + + channelID := "channelID" + expectedPostList := model.NewPostList() + expectedPostList.AddPost(&model.Post{ChannelId: channelID}) + + api.On("GetPostsForChannel", channelID, 0, 0).Return(expectedPostList, nil) + + actualPostList, err := client.Post.GetPostsForChannel(channelID, 0, 0) + require.NoError(t, err) + assert.Equal(t, expectedPostList, actualPostList) + }) + + t.Run("failure", func(t *testing.T) { + api := &plugintest.API{} + defer api.AssertExpectations(t) + client := pluginapi.NewClient(api, &plugintest.Driver{}) + + channelID := "channelID" + api.On("GetPostsForChannel", channelID, 0, 0).Return(nil, newAppError()) + + actualPostList, err := client.Post.GetPostsForChannel(channelID, 0, 0) + require.EqualError(t, err, "here: id, an error occurred") + assert.Nil(t, actualPostList) + }) +} + +func TestSearchPostsInTeam(t *testing.T) { + t.Run("success", func(t *testing.T) { + api := &plugintest.API{} + defer api.AssertExpectations(t) + client := pluginapi.NewClient(api, &plugintest.Driver{}) + + teamID := "teamID" + searchParams := []*model.SearchParams{{InChannels: []string{"channelID"}}} + expectedPosts := []*model.Post{{ChannelId: "channelID"}} + api.On("SearchPostsInTeam", teamID, searchParams).Return(expectedPosts, nil) + + actualPostList, err := client.Post.SearchPostsInTeam(teamID, searchParams) + require.NoError(t, err) + assert.Equal(t, expectedPosts, actualPostList) + }) + + t.Run("failure", func(t *testing.T) { + api := &plugintest.API{} + defer api.AssertExpectations(t) + client := pluginapi.NewClient(api, &plugintest.Driver{}) + + teamID := "teamID" + searchParams := []*model.SearchParams{{InChannels: []string{"channelID"}}} + api.On("SearchPostsInTeam", teamID, searchParams).Return(nil, newAppError()) + + actualPostList, err := client.Post.SearchPostsInTeam(teamID, searchParams) + require.EqualError(t, err, "here: id, an error occurred") + assert.Nil(t, actualPostList) + }) +} + +func TestAddReaction(t *testing.T) { + t.Run("success", func(t *testing.T) { + api := &plugintest.API{} + defer api.AssertExpectations(t) + client := pluginapi.NewClient(api, &plugintest.Driver{}) + + in := &model.Reaction{ + PostId: "postId", + } + api.On("AddReaction", in).Return(in, nil) + + err := client.Post.AddReaction(in) + require.NoError(t, err) + }) + + t.Run("failure", func(t *testing.T) { + api := &plugintest.API{} + defer api.AssertExpectations(t) + client := pluginapi.NewClient(api, &plugintest.Driver{}) + + in := &model.Reaction{ + PostId: "postId", + } + api.On("AddReaction", in).Return(nil, newAppError()) + + err := client.Post.AddReaction(in) + require.EqualError(t, err, "here: id, an error occurred") + }) +} + +func TestGetReactions(t *testing.T) { + t.Run("success", func(t *testing.T) { + api := &plugintest.API{} + defer api.AssertExpectations(t) + client := pluginapi.NewClient(api, &plugintest.Driver{}) + + postID := "postID" + expectedReactions := []*model.Reaction{ + {PostId: postID, UserId: "user1"}, + {PostId: postID, UserId: "user2"}, + } + api.On("GetReactions", postID).Return(expectedReactions, nil) + + actualReactions, err := client.Post.GetReactions(postID) + require.NoError(t, err) + assert.Equal(t, expectedReactions, actualReactions) + }) + + t.Run("failure", func(t *testing.T) { + api := &plugintest.API{} + defer api.AssertExpectations(t) + client := pluginapi.NewClient(api, &plugintest.Driver{}) + + postID := "postID" + api.On("GetReactions", postID).Return(nil, newAppError()) + + actualReactions, err := client.Post.GetReactions(postID) + require.EqualError(t, err, "here: id, an error occurred") + assert.Nil(t, actualReactions) + }) +} + +func TestDeleteReaction(t *testing.T) { + t.Run("success", func(t *testing.T) { + api := &plugintest.API{} + defer api.AssertExpectations(t) + client := pluginapi.NewClient(api, &plugintest.Driver{}) + + reaction := &model.Reaction{ + PostId: "postId", + } + api.On("RemoveReaction", reaction).Return(nil) + + err := client.Post.RemoveReaction(reaction) + require.NoError(t, err) + }) + + t.Run("failure", func(t *testing.T) { + api := &plugintest.API{} + defer api.AssertExpectations(t) + client := pluginapi.NewClient(api, &plugintest.Driver{}) + + reaction := &model.Reaction{ + PostId: "postId", + } + api.On("RemoveReaction", reaction).Return(newAppError()) + + err := client.Post.RemoveReaction(reaction) + require.EqualError(t, err, "here: id, an error occurred") + }) +} + +func TestSearchTeamPosts(t *testing.T) { + t.Run("success", func(t *testing.T) { + api := &plugintest.API{} + defer api.AssertExpectations(t) + client := pluginapi.NewClient(api, &plugintest.Driver{}) + + api.On("SearchPostsInTeam", "1", []*model.SearchParams{{Terms: "2"}, {Terms: "3"}}). + Return([]*model.Post{{Id: "3"}, {Id: "4"}}, nil) + + posts, err := client.Post.SearchPostsInTeam("1", []*model.SearchParams{{Terms: "2"}, {Terms: "3"}}) + require.NoError(t, err) + require.Equal(t, []*model.Post{{Id: "3"}, {Id: "4"}}, posts) + }) + + t.Run("failure", func(t *testing.T) { + api := &plugintest.API{} + defer api.AssertExpectations(t) + client := pluginapi.NewClient(api, &plugintest.Driver{}) + + appErr := model.NewAppError("here", "id", nil, "an error occurred", http.StatusInternalServerError) + + api.On("SearchPostsInTeam", "1", []*model.SearchParams{{Terms: "2"}, {Terms: "3"}}). + Return(nil, appErr) + + posts, err := client.Post.SearchPostsInTeam("1", []*model.SearchParams{{Terms: "2"}, {Terms: "3"}}) + require.Equal(t, appErr, err) + require.Len(t, posts, 0) + }) +} + +func TestShouldProcessMessage(t *testing.T) { + expectedBotID := model.NewId() + + setupAPI := func() *plugintest.API { + return &plugintest.API{} + } + + t.Run("should not respond to itself", func(t *testing.T) { + api := setupAPI() + api.On("KVGet", plugin.BotUserKey).Return([]byte(expectedBotID), nil) + client := pluginapi.NewClient(api, &plugintest.Driver{}) + + shouldProcessMessage, err := client.Post.ShouldProcessMessage( + &model.Post{Type: model.PostTypeHeaderChange, UserId: expectedBotID}, + pluginapi.AllowSystemMessages(), + pluginapi.AllowBots(), + ) + + assert.NoError(t, err) + assert.False(t, shouldProcessMessage) + }) + + t.Run("should not process as the post is generated by system", func(t *testing.T) { + api := setupAPI() + api.On("KVGet", plugin.BotUserKey).Return([]byte(expectedBotID), nil) + client := pluginapi.NewClient(api, &plugintest.Driver{}) + + shouldProcessMessage, err := client.Post.ShouldProcessMessage( + &model.Post{Type: model.PostTypeHeaderChange}, + ) + + assert.NoError(t, err) + assert.False(t, shouldProcessMessage) + }) + + t.Run("should not process as the post is sent to another channel", func(t *testing.T) { + channelID := "channel-id" + api := setupAPI() + api.On("GetChannel", channelID).Return(&model.Channel{Id: channelID, Type: model.ChannelTypeGroup}, nil) + api.On("KVGet", plugin.BotUserKey).Return([]byte(expectedBotID), nil) + client := pluginapi.NewClient(api, &plugintest.Driver{}) + + shouldProcessMessage, err := client.Post.ShouldProcessMessage( + &model.Post{ChannelId: channelID}, pluginapi.AllowSystemMessages(), + pluginapi.AllowBots(), + pluginapi.FilterChannelIDs([]string{"another-channel-id"}), + ) + + assert.NoError(t, err) + assert.False(t, shouldProcessMessage) + }) + + t.Run("should not process as the post is created by bot", func(t *testing.T) { + userID := "user-id" + channelID := "1" + api := setupAPI() + api.On("GetUser", userID).Return(&model.User{IsBot: true}, nil) + api.On("KVGet", plugin.BotUserKey).Return([]byte(expectedBotID), nil) + client := pluginapi.NewClient(api, &plugintest.Driver{}) + + shouldProcessMessage, err := client.Post.ShouldProcessMessage( + &model.Post{UserId: userID, ChannelId: channelID}, + pluginapi.AllowSystemMessages(), + pluginapi.FilterUserIDs([]string{"another-user-id"}), + ) + + assert.NoError(t, err) + assert.False(t, shouldProcessMessage) + }) + + t.Run("should not process the message as the post is not in bot dm channel", func(t *testing.T) { + userID := "user-id" + channelID := "1" + channel := model.Channel{ + Name: "user1__" + expectedBotID, + Type: model.ChannelTypeOpen, + } + api := setupAPI() + api.On("GetChannel", channelID).Return(&channel, nil) + api.On("KVGet", plugin.BotUserKey).Return([]byte(expectedBotID), nil) + client := pluginapi.NewClient(api, &plugintest.Driver{}) + + shouldProcessMessage, err := client.Post.ShouldProcessMessage( + &model.Post{UserId: userID, ChannelId: channelID}, + pluginapi.AllowSystemMessages(), + pluginapi.AllowBots(), + pluginapi.OnlyBotDMs(), + ) + + assert.NoError(t, err) + assert.False(t, shouldProcessMessage) + }) + + t.Run("should process the message", func(t *testing.T) { + channelID := "1" + api := setupAPI() + api.On("KVGet", plugin.BotUserKey).Return([]byte(expectedBotID), nil) + client := pluginapi.NewClient(api, &plugintest.Driver{}) + + shouldProcessMessage, err := client.Post.ShouldProcessMessage( + &model.Post{UserId: "1", Type: model.PostTypeHeaderChange, ChannelId: channelID}, + pluginapi.AllowSystemMessages(), + pluginapi.FilterChannelIDs([]string{channelID}), + pluginapi.AllowBots(), + pluginapi.FilterUserIDs([]string{"1"}), + ) + + assert.NoError(t, err) + assert.True(t, shouldProcessMessage) + }) + + t.Run("should process the message for plugin without a bot", func(t *testing.T) { + channelID := "1" + api := setupAPI() + api.On("KVGet", plugin.BotUserKey).Return(nil, nil) + client := pluginapi.NewClient(api, &plugintest.Driver{}) + + shouldProcessMessage, err := client.Post.ShouldProcessMessage( + &model.Post{UserId: "1", Type: model.PostTypeHeaderChange, ChannelId: channelID}, + pluginapi.AllowSystemMessages(), + pluginapi.FilterChannelIDs([]string{channelID}), + pluginapi.AllowBots(), + pluginapi.FilterUserIDs([]string{"1"}), + ) + + assert.NoError(t, err) + assert.True(t, shouldProcessMessage) + }) + + t.Run("should process the message when filter channel and filter users list is empty", func(t *testing.T) { + channelID := "1" + api := setupAPI() + channel := model.Channel{ + Name: "user1__" + expectedBotID, + Type: model.ChannelTypeDirect, + } + api.On("GetChannel", channelID).Return(&channel, nil) + api.On("KVGet", plugin.BotUserKey).Return([]byte(expectedBotID), nil) + client := pluginapi.NewClient(api, &plugintest.Driver{}) + + shouldProcessMessage, err := client.Post.ShouldProcessMessage( + &model.Post{UserId: "1", Type: model.PostTypeHeaderChange, ChannelId: channelID}, + pluginapi.AllowSystemMessages(), + pluginapi.AllowBots(), + ) + + assert.NoError(t, err) + assert.True(t, shouldProcessMessage) + }) + + t.Run("should not process the message which have from_webhook", func(t *testing.T) { + channelID := "1" + api := setupAPI() + api.On("GetChannel", channelID).Return(&model.Channel{Id: channelID, Type: model.ChannelTypeGroup}, nil) + + api.On("KVGet", plugin.BotUserKey).Return([]byte(expectedBotID), nil) + + client := pluginapi.NewClient(api, &plugintest.Driver{}) + shouldProcessMessage, err := client.Post.ShouldProcessMessage( + &model.Post{ChannelId: channelID, Props: model.StringInterface{"from_webhook": "true"}}, + pluginapi.AllowBots(), + ) + + assert.NoError(t, err) + assert.False(t, shouldProcessMessage) + }) + + t.Run("should process the message which have from_webhook with allow webhook plugin", func(t *testing.T) { + channelID := "1" + api := setupAPI() + api.On("GetChannel", channelID).Return(&model.Channel{Id: channelID, Type: model.ChannelTypeGroup}, nil) + + api.On("KVGet", plugin.BotUserKey).Return([]byte(expectedBotID), nil) + client := pluginapi.NewClient(api, &plugintest.Driver{}) + + shouldProcessMessage, err := client.Post.ShouldProcessMessage( + &model.Post{ChannelId: channelID, Props: model.StringInterface{"from_webhook": "true"}}, + pluginapi.AllowBots(), + pluginapi.AllowWebhook(), + ) + + assert.NoError(t, err) + assert.True(t, shouldProcessMessage) + }) + + t.Run("should process the message where from_webhook is not set", func(t *testing.T) { + channelID := "1" + api := setupAPI() + api.On("GetChannel", channelID).Return(&model.Channel{Id: channelID, Type: model.ChannelTypeGroup}, nil) + + api.On("KVGet", plugin.BotUserKey).Return([]byte(expectedBotID), nil) + client := pluginapi.NewClient(api, &plugintest.Driver{}) + + shouldProcessMessage, err := client.Post.ShouldProcessMessage( + &model.Post{ChannelId: channelID}, + pluginapi.AllowBots(), + ) + + assert.NoError(t, err) + assert.True(t, shouldProcessMessage) + }) + + t.Run("should process the message which have from_webhook false", func(t *testing.T) { + channelID := "1" + api := setupAPI() + api.On("GetChannel", channelID).Return(&model.Channel{Id: channelID, Type: model.ChannelTypeGroup}, nil) + + api.On("KVGet", plugin.BotUserKey).Return([]byte(expectedBotID), nil) + client := pluginapi.NewClient(api, &plugintest.Driver{}) + + shouldProcessMessage, err := client.Post.ShouldProcessMessage( + &model.Post{ChannelId: channelID, Props: model.StringInterface{"from_webhook": "false"}}, + pluginapi.AllowBots(), + ) + + assert.NoError(t, err) + assert.True(t, shouldProcessMessage) + }) + + t.Run("should process the message when we pass the botId as input", func(t *testing.T) { + userID := "user-id" + channelID := "1" + api := setupAPI() + api.On("GetChannel", channelID).Return(&model.Channel{Id: channelID, Type: model.ChannelTypeGroup}, nil) + + api.On("GetUser", userID).Return(&model.User{IsBot: false}, nil) + + client := pluginapi.NewClient(api, &plugintest.Driver{}) + + shouldProcessMessage, err := client.Post.ShouldProcessMessage( + &model.Post{ChannelId: channelID, UserId: userID}, + pluginapi.BotID(expectedBotID), + ) + assert.NoError(t, err) + + assert.True(t, shouldProcessMessage) + }) +} diff --git a/server/public/pluginapi/session.go b/server/public/pluginapi/session.go new file mode 100644 index 00000000000..05c11408c81 --- /dev/null +++ b/server/public/pluginapi/session.go @@ -0,0 +1,43 @@ +package pluginapi + +import ( + "github.com/mattermost/mattermost/server/public/model" + "github.com/mattermost/mattermost/server/public/plugin" +) + +// SessionService exposes methods to manipulate groups. +type SessionService struct { + api plugin.API +} + +// Get returns the session object for the Session ID +// +// Minimum server version: 5.2 +func (s *SessionService) Get(id string) (*model.Session, error) { + session, appErr := s.api.GetSession(id) + + return session, normalizeAppErr(appErr) +} + +// Create creates a new user session. +// +// Minimum server version: 6.2 +func (s *SessionService) Create(session *model.Session) (*model.Session, error) { + session, appErr := s.api.CreateSession(session) + + return session, normalizeAppErr(appErr) +} + +// ExtendSessionExpiry extends the duration of an existing session. +// +// Minimum server version: 6.2 +func (s *SessionService) ExtendExpiry(sessionID string, newExpiry int64) error { + return normalizeAppErr(s.api.ExtendSessionExpiry(sessionID, newExpiry)) +} + +// RevokeSession revokes an existing user session. +// +// Minimum server version: 6.2 +func (s *SessionService) Revoke(sessionID string) error { + return normalizeAppErr(s.api.RevokeSession(sessionID)) +} diff --git a/server/public/pluginapi/slashcommand.go b/server/public/pluginapi/slashcommand.go new file mode 100644 index 00000000000..7fe939e5432 --- /dev/null +++ b/server/public/pluginapi/slashcommand.go @@ -0,0 +1,100 @@ +package pluginapi + +import ( + "github.com/mattermost/mattermost/server/public/model" + "github.com/mattermost/mattermost/server/public/plugin" +) + +// SlashCommandService exposes methods to manipulate slash commands. +type SlashCommandService struct { + api plugin.API +} + +// Register registers a custom slash command. When the command is triggered, your plugin +// can fulfill it via the ExecuteCommand hook. +// +// Minimum server version: 5.2 +func (c *SlashCommandService) Register(command *model.Command) error { + return c.api.RegisterCommand(command) +} + +// Unregister unregisters a command previously registered via Register. +// +// Minimum server version: 5.2 +func (c *SlashCommandService) Unregister(teamID, trigger string) error { + return c.api.UnregisterCommand(teamID, trigger) +} + +// Execute executes a slash command. +// +// Minimum server version: 5.26 +func (c *SlashCommandService) Execute(command *model.CommandArgs) (*model.CommandResponse, error) { + return c.api.ExecuteSlashCommand(command) +} + +// Create creates a server-owned slash command that is not handled by the plugin +// itself, and which will persist past the life of the plugin. The command will have its +// CreatorId set to "" and its PluginId set to the id of the plugin that created it. +// +// Minimum server version: 5.28 +func (c *SlashCommandService) Create(command *model.Command) (*model.Command, error) { + return c.api.CreateCommand(command) +} + +// List returns the list of all slash commands for teamID. E.g., custom commands +// (those created through the integrations menu, the REST api, or the plugin api CreateCommand), +// plugin commands (those created with plugin api RegisterCommand), and builtin commands +// (those added internally through RegisterCommandProvider). +// +// Minimum server version: 5.28 +func (c *SlashCommandService) List(teamID string) ([]*model.Command, error) { + return c.api.ListCommands(teamID) +} + +// ListCustom returns the list of slash commands for teamID that where created +// through the integrations menu, the REST api, or the plugin api CreateCommand. +// +// Minimum server version: 5.28 +func (c *SlashCommandService) ListCustom(teamID string) ([]*model.Command, error) { + return c.api.ListCustomCommands(teamID) +} + +// ListPlugin returns the list of slash commands for teamID that were created +// with the plugin api RegisterCommand. +// +// Minimum server version: 5.28 +func (c *SlashCommandService) ListPlugin(teamID string) ([]*model.Command, error) { + return c.api.ListPluginCommands(teamID) +} + +// ListBuiltIn returns the list of slash commands that are builtin commands +// (those added internally through RegisterCommandProvider). +// +// Minimum server version: 5.28 +func (c *SlashCommandService) ListBuiltIn() ([]*model.Command, error) { + return c.api.ListBuiltInCommands() +} + +// Get returns the command definition based on a command id string. +// +// Minimum server version: 5.28 +func (c *SlashCommandService) Get(commandID string) (*model.Command, error) { + return c.api.GetCommand(commandID) +} + +// Update updates a single command (identified by commandID) with the information provided in the +// updatedCmd model.Command struct. The following fields in the command cannot be updated: +// Id, Token, CreateAt, DeleteAt, and PluginId. If updatedCmd.TeamId is blank, it +// will be set to commandID's TeamId. +// +// Minimum server version: 5.28 +func (c *SlashCommandService) Update(commandID string, updatedCmd *model.Command) (*model.Command, error) { + return c.api.UpdateCommand(commandID, updatedCmd) +} + +// Delete deletes a slash command (identified by commandID). +// +// Minimum server version: 5.28 +func (c *SlashCommandService) Delete(commandID string) error { + return c.api.DeleteCommand(commandID) +} diff --git a/server/public/pluginapi/store.go b/server/public/pluginapi/store.go new file mode 100644 index 00000000000..c0daee7c808 --- /dev/null +++ b/server/public/pluginapi/store.go @@ -0,0 +1,117 @@ +package pluginapi + +import ( + "database/sql" + "sync" + + // import sql drivers + _ "github.com/go-sql-driver/mysql" + _ "github.com/lib/pq" + + "github.com/mattermost/mattermost/server/public/plugin" + "github.com/mattermost/mattermost/server/public/shared/driver" + "github.com/pkg/errors" +) + +// StoreService exposes the underlying database. +type StoreService struct { + initialized bool + api plugin.API + driver plugin.Driver + mutex sync.Mutex + + masterDB *sql.DB + replicaDB *sql.DB +} + +// GetMasterDB gets the master database handle. +// +// Minimum server version: 5.16 +func (s *StoreService) GetMasterDB() (*sql.DB, error) { + s.mutex.Lock() + defer s.mutex.Unlock() + + if err := s.initialize(); err != nil { + return nil, err + } + + return s.masterDB, nil +} + +// GetReplicaDB gets the replica database handle. +// Returns masterDB if a replica is not configured. +// +// Minimum server version: 5.16 +func (s *StoreService) GetReplicaDB() (*sql.DB, error) { + s.mutex.Lock() + defer s.mutex.Unlock() + + if err := s.initialize(); err != nil { + return nil, err + } + + if s.replicaDB != nil { + return s.replicaDB, nil + } + + return s.masterDB, nil +} + +// Close closes any open resources. This method is idempotent. +func (s *StoreService) Close() error { + s.mutex.Lock() + defer s.mutex.Unlock() + + if !s.initialized { + return nil + } + + if err := s.masterDB.Close(); err != nil { + return err + } + + if s.replicaDB != nil { + if err := s.replicaDB.Close(); err != nil { + return err + } + } + + return nil +} + +// DriverName returns the driver name for the datasource. +func (s *StoreService) DriverName() string { + return *s.api.GetConfig().SqlSettings.DriverName +} + +func (s *StoreService) initialize() error { + if s.initialized { + return nil + } + + if s.driver == nil { + return errors.New("no db driver was provided") + } + + config := s.api.GetUnsanitizedConfig() + + // Set up master db + db := sql.OpenDB(driver.NewConnector(s.driver, true)) + if err := db.Ping(); err != nil { + return errors.Wrap(err, "failed to connect to master db") + } + s.masterDB = db + + // Set up replica db + if len(config.SqlSettings.DataSourceReplicas) > 0 { + db := sql.OpenDB(driver.NewConnector(s.driver, false)) + if err := db.Ping(); err != nil { + return errors.Wrap(err, "failed to connect to replica db") + } + s.replicaDB = db + } + + s.initialized = true + + return nil +} diff --git a/server/public/pluginapi/store_test.go b/server/public/pluginapi/store_test.go new file mode 100644 index 00000000000..78b2b98f476 --- /dev/null +++ b/server/public/pluginapi/store_test.go @@ -0,0 +1,109 @@ +package pluginapi_test + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/mattermost/mattermost/server/public/model" + "github.com/mattermost/mattermost/server/public/plugin/plugintest" + "github.com/mattermost/mattermost/server/public/pluginapi" +) + +func TestStore(t *testing.T) { + t.Run("master db singleton", func(t *testing.T) { + config := &model.Config{ + SqlSettings: model.SqlSettings{ + DriverName: model.NewString("test"), + DataSource: model.NewString("TestStore-master-db"), + }, + } + + api := &plugintest.API{} + defer api.AssertExpectations(t) + api.On("GetUnsanitizedConfig").Return(config) + + driver := &plugintest.Driver{} + driver.On("Conn", true).Return("test", nil) + driver.On("ConnPing", "test").Return(nil) + driver.On("ConnClose", "test").Return(nil) + + store := pluginapi.NewClient(api, driver).Store + + db1, err := store.GetMasterDB() + require.NoError(t, err) + require.NotNil(t, db1) + + db2, err := store.GetMasterDB() + require.NoError(t, err) + require.NotNil(t, db2) + + require.Same(t, db1, db2) + require.NoError(t, store.Close()) + }) + + t.Run("master db fallback", func(t *testing.T) { + config := &model.Config{ + SqlSettings: model.SqlSettings{ + DriverName: model.NewString("ramsql"), + DataSource: model.NewString("TestStore-master-db"), + ConnMaxLifetimeMilliseconds: model.NewInt(2), + }, + } + + driver := &plugintest.Driver{} + driver.On("Conn", true).Return("test", nil) + driver.On("ConnPing", "test").Return(nil) + driver.On("ConnClose", "test").Return(nil) + + api := &plugintest.API{} + defer api.AssertExpectations(t) + store := pluginapi.NewClient(api, driver).Store + + api.On("GetUnsanitizedConfig").Return(config) + masterDB, err := store.GetMasterDB() + require.NoError(t, err) + require.NotNil(t, masterDB) + + // No replica is set up, should fallback to master + replicaDB, err := store.GetReplicaDB() + require.NoError(t, err) + require.Same(t, replicaDB, masterDB) + + require.NoError(t, store.Close()) + }) + + t.Run("replica db singleton", func(t *testing.T) { + config := &model.Config{ + SqlSettings: model.SqlSettings{ + DriverName: model.NewString("ramsql"), + DataSource: model.NewString("TestStore-master-db"), + DataSourceReplicas: []string{"TestStore-master-db"}, + ConnMaxLifetimeMilliseconds: model.NewInt(2), + }, + } + + api := &plugintest.API{} + defer api.AssertExpectations(t) + api.On("GetUnsanitizedConfig").Return(config) + + driver := &plugintest.Driver{} + driver.On("Conn", true).Return("test", nil) + driver.On("Conn", false).Return("test", nil) + driver.On("ConnPing", "test").Return(nil) + driver.On("ConnClose", "test").Return(nil) + + store := pluginapi.NewClient(api, driver).Store + + db1, err := store.GetReplicaDB() + require.NoError(t, err) + require.NotNil(t, db1) + + db2, err := store.GetReplicaDB() + require.NoError(t, err) + require.NotNil(t, db2) + + require.Same(t, db1, db2) + require.NoError(t, store.Close()) + }) +} diff --git a/server/public/pluginapi/system.go b/server/public/pluginapi/system.go new file mode 100644 index 00000000000..e548d5d8a85 --- /dev/null +++ b/server/public/pluginapi/system.go @@ -0,0 +1,124 @@ +package pluginapi + +import ( + "net/url" + "path" + "time" + + "github.com/blang/semver/v4" + "github.com/pkg/errors" + + "github.com/mattermost/mattermost/server/public/model" + "github.com/mattermost/mattermost/server/public/plugin" +) + +// SystemService exposes methods to query system properties. +type SystemService struct { + api plugin.API +} + +// GetManifest returns the manifest from the plugin bundle. +// +// Minimum server version: 5.10 +func (s *SystemService) GetManifest() (*model.Manifest, error) { + p, err := s.api.GetBundlePath() + if err != nil { + return nil, err + } + + m, _, err := model.FindManifest(p) + if err != nil { + return nil, errors.Wrap(err, "failed to find and open manifest") + } + + return m, nil +} + +// GetBundlePath returns the absolute path where the plugin's bundle was unpacked. +// +// Minimum server version: 5.10 +func (s *SystemService) GetBundlePath() (string, error) { + return s.api.GetBundlePath() +} + +// GetPluginAssetURL builds a URL to the given asset in the assets directory. +// Use this URL to link to assets from the webapp, or for third-party integrations with your plugin. +// +// Minimum server version: 5.2 +func (s *SystemService) GetPluginAssetURL(pluginID, asset string) (string, error) { + if pluginID == "" { + return "", errors.New("empty pluginID provided") + } + + if asset == "" { + return "", errors.New("empty asset name provided") + } + + siteURL := *s.api.GetConfig().ServiceSettings.SiteURL + if siteURL == "" { + return "", errors.New("no SiteURL configured by the server") + } + + u, err := url.Parse(siteURL + path.Join("/", pluginID, asset)) + if err != nil { + return "", err + } + + return u.String(), nil +} + +// GetLicense returns the current license used by the Mattermost server. Returns nil if the +// the server does not have a license. +// +// Minimum server version: 5.10 +func (s *SystemService) GetLicense() *model.License { + return s.api.GetLicense() +} + +// GetServerVersion return the current Mattermost server version +// +// Minimum server version: 5.4 +func (s *SystemService) GetServerVersion() string { + return s.api.GetServerVersion() +} + +// IsEnterpriseReady returns true if the Mattermost server is configured as Enterprise Ready. +// +// Minimum server version: 6.1 +func (s *SystemService) IsEnterpriseReady() bool { + return s.api.IsEnterpriseReady() +} + +// GetSystemInstallDate returns the time that Mattermost was first installed and ran. +// +// Minimum server version: 5.10 +func (s *SystemService) GetSystemInstallDate() (time.Time, error) { + installDateMS, appErr := s.api.GetSystemInstallDate() + installDate := time.Unix(0, installDateMS*int64(time.Millisecond)) + + return installDate, normalizeAppErr(appErr) +} + +// GetDiagnosticID returns a unique identifier used by the server for diagnostic reports. +// +// Minimum server version: 5.10 +func (s *SystemService) GetDiagnosticID() string { + // TODO: Consider deprecating/rewriting in favor of just using GetUnsanitizedConfig(). + return s.api.GetDiagnosticId() +} + +// RequestTrialLicense requests a trial license and installs it in the server. +// If the server version is lower than 5.36.0, an error is returned. +// +// Minimum server version: 5.36 +func (s *SystemService) RequestTrialLicense(requesterID string, users int, termsAccepted, receiveEmailsAccepted bool) error { + currentVersion := semver.MustParse(s.api.GetServerVersion()) + requiredVersion := semver.MustParse("5.36.0") + + if currentVersion.LT(requiredVersion) { + return errors.Errorf("current server version is lower than 5.36") + } + + err := s.api.RequestTrialLicense(requesterID, users, termsAccepted, receiveEmailsAccepted) + return normalizeAppErr(err) +} diff --git a/server/public/pluginapi/system_test.go b/server/public/pluginapi/system_test.go new file mode 100644 index 00000000000..5597365f71e --- /dev/null +++ b/server/public/pluginapi/system_test.go @@ -0,0 +1,104 @@ +package pluginapi_test + +import ( + "os" + "path/filepath" + "testing" + + "github.com/pkg/errors" + "github.com/stretchr/testify/require" + + "github.com/mattermost/mattermost/server/public/model" + "github.com/mattermost/mattermost/server/public/plugin/plugintest" + "github.com/mattermost/mattermost/server/public/pluginapi" +) + +func TestGetManifest(t *testing.T) { + t.Run("valid manifest", func(t *testing.T) { + content := []byte(` + { + "id": "some.id", + "name": "Some Name" + } + `) + expectedManifest := &model.Manifest{ + Id: "some.id", + Name: "Some Name", + } + + dir, err := os.MkdirTemp("", "") + require.NoError(t, err) + defer os.RemoveAll(dir) + + tmpfn := filepath.Join(dir, "plugin.json") + //nolint:gosec //only used in tests + err = os.WriteFile(tmpfn, content, 0o666) + require.NoError(t, err) + + api := &plugintest.API{} + api.On("GetBundlePath").Return(dir, nil) + defer api.AssertExpectations(t) + client := pluginapi.NewClient(api, &plugintest.Driver{}) + m, err := client.System.GetManifest() + require.NoError(t, err) + require.Equal(t, expectedManifest, m) + + // Altering the pointer doesn't alter the result + m.Id = "new.id" + + m2, err := client.System.GetManifest() + require.NoError(t, err) + require.Equal(t, expectedManifest, m2) + }) + + t.Run("GetBundlePath fails", func(t *testing.T) { + api := &plugintest.API{} + api.On("GetBundlePath").Return("", errors.New("")) + defer api.AssertExpectations(t) + client := pluginapi.NewClient(api, &plugintest.Driver{}) + m, err := client.System.GetManifest() + require.Error(t, err) + require.Nil(t, m) + }) + + t.Run("No manifest found", func(t *testing.T) { + dir, err := os.MkdirTemp("", "") + require.NoError(t, err) + defer os.RemoveAll(dir) + + api := &plugintest.API{} + api.On("GetBundlePath").Return(dir, nil) + defer api.AssertExpectations(t) + client := pluginapi.NewClient(api, &plugintest.Driver{}) + m, err := client.System.GetManifest() + require.Error(t, err) + require.Nil(t, m) + }) +} + +func TestRequestTrialLicense(t *testing.T) { + t.Run("Server version incompatible", func(t *testing.T) { + api := &plugintest.API{} + defer api.AssertExpectations(t) + client := pluginapi.NewClient(api, &plugintest.Driver{}) + + api.On("GetServerVersion").Return("5.35.0") + err := client.System.RequestTrialLicense("requesterID", 10, true, true) + + require.Error(t, err) + require.Equal(t, "current server version is lower than 5.36", err.Error()) + }) + + t.Run("Server version compatible", func(t *testing.T) { + api := &plugintest.API{} + defer api.AssertExpectations(t) + client := pluginapi.NewClient(api, &plugintest.Driver{}) + + api.On("GetServerVersion").Return("5.36.0") + api.On("RequestTrialLicense", "requesterID", 10, true, true).Return(nil) + + err := client.System.RequestTrialLicense("requesterID", 10, true, true) + + require.NoError(t, err) + }) +} diff --git a/server/public/pluginapi/team.go b/server/public/pluginapi/team.go new file mode 100644 index 00000000000..b3fb9f05cf2 --- /dev/null +++ b/server/public/pluginapi/team.go @@ -0,0 +1,231 @@ +package pluginapi + +import ( + "bytes" + "io" + + "github.com/mattermost/mattermost/server/public/model" + "github.com/mattermost/mattermost/server/public/plugin" +) + +// TeamService exposes methods to manipulate teams and their members. +type TeamService struct { + api plugin.API +} + +// Get gets a team. +// +// Minimum server version: 5.2 +func (t *TeamService) Get(teamID string) (*model.Team, error) { + team, appErr := t.api.GetTeam(teamID) + + return team, normalizeAppErr(appErr) +} + +// GetByName gets a team by its name. +// +// Minimum server version: 5.2 +func (t *TeamService) GetByName(name string) (*model.Team, error) { + team, appErr := t.api.GetTeamByName(name) + + return team, normalizeAppErr(appErr) +} + +// TeamListOption is used to filter team listing. +type TeamListOption func(*ListTeamsOptions) + +// ListTeamsOptions holds options about filter out team listing. +type ListTeamsOptions struct { + UserID string +} + +// FilterTeamsByUser option is used to filter teams by user. +func FilterTeamsByUser(userID string) TeamListOption { + return func(o *ListTeamsOptions) { + o.UserID = userID + } +} + +// List gets a list of teams by options. +// +// Minimum server version: 5.2 +// Minimum server version when LimitTeamsToUser() option is used: 5.6 +func (t *TeamService) List(options ...TeamListOption) ([]*model.Team, error) { + opts := ListTeamsOptions{} + for _, o := range options { + o(&opts) + } + + var teams []*model.Team + var appErr *model.AppError + if opts.UserID != "" { + teams, appErr = t.api.GetTeamsForUser(opts.UserID) + } else { + teams, appErr = t.api.GetTeams() + } + + return teams, normalizeAppErr(appErr) +} + +// Search search a team. +// +// Minimum server version: 5.8 +func (t *TeamService) Search(term string) ([]*model.Team, error) { + teams, appErr := t.api.SearchTeams(term) + + return teams, normalizeAppErr(appErr) +} + +// Create creates a team. +// +// Minimum server version: 5.2 +func (t *TeamService) Create(team *model.Team) error { + createdTeam, appErr := t.api.CreateTeam(team) + if appErr != nil { + return normalizeAppErr(appErr) + } + + *team = *createdTeam + + return nil +} + +// Update updates a team. +// +// Minimum server version: 5.2 +func (t *TeamService) Update(team *model.Team) error { + updatedTeam, appErr := t.api.UpdateTeam(team) + if appErr != nil { + return normalizeAppErr(appErr) + } + + *team = *updatedTeam + + return nil +} + +// Delete deletes a team. +// +// Minimum server version: 5.2 +func (t *TeamService) Delete(teamID string) error { + return normalizeAppErr(t.api.DeleteTeam(teamID)) +} + +// GetIcon gets the team icon. +// +// Minimum server version: 5.6 +func (t *TeamService) GetIcon(teamID string) (io.Reader, error) { + contentBytes, appErr := t.api.GetTeamIcon(teamID) + if appErr != nil { + return nil, normalizeAppErr(appErr) + } + + return bytes.NewReader(contentBytes), nil +} + +// SetIcon sets the team icon. +// +// Minimum server version: 5.6 +func (t *TeamService) SetIcon(teamID string, content io.Reader) error { + contentBytes, err := io.ReadAll(content) + if err != nil { + return err + } + + return normalizeAppErr(t.api.SetTeamIcon(teamID, contentBytes)) +} + +// DeleteIcon removes the team icon. +// +// Minimum server version: 5.6 +func (t *TeamService) DeleteIcon(teamID string) error { + return normalizeAppErr(t.api.RemoveTeamIcon(teamID)) +} + +// GetUsers lists users of the team. +// +// Minimum server version: 5.6 +func (t *TeamService) ListUsers(teamID string, page, count int) ([]*model.User, error) { + users, appErr := t.api.GetUsersInTeam(teamID, page, count) + + return users, normalizeAppErr(appErr) +} + +// ListUnreadForUser gets the unread message and mention counts for each team to which the given user belongs. +// +// Minimum server version: 5.6 +func (t *TeamService) ListUnreadForUser(userID string) ([]*model.TeamUnread, error) { + teamUnreads, appErr := t.api.GetTeamsUnreadForUser(userID) + + return teamUnreads, normalizeAppErr(appErr) +} + +// GetMember returns a specific membership. +// +// Minimum server version: 5.2 +func (t *TeamService) GetMember(teamID, userID string) (*model.TeamMember, error) { + teamMember, appErr := t.api.GetTeamMember(teamID, userID) + + return teamMember, normalizeAppErr(appErr) +} + +// ListMembers returns the memberships of a specific team. +// +// Minimum server version: 5.2 +func (t *TeamService) ListMembers(teamID string, page, perPage int) ([]*model.TeamMember, error) { + teamMembers, appErr := t.api.GetTeamMembers(teamID, page, perPage) + + return teamMembers, normalizeAppErr(appErr) +} + +// ListMembersForUser returns all team memberships for a user. +// +// Minimum server version: 5.10 +func (t *TeamService) ListMembersForUser(userID string, page, perPage int) ([]*model.TeamMember, error) { + teamMembers, appErr := t.api.GetTeamMembersForUser(userID, page, perPage) + + return teamMembers, normalizeAppErr(appErr) +} + +// CreateMember creates a team membership. +// +// Minimum server version: 5.2 +func (t *TeamService) CreateMember(teamID, userID string) (*model.TeamMember, error) { + teamMember, appErr := t.api.CreateTeamMember(teamID, userID) + + return teamMember, normalizeAppErr(appErr) +} + +// CreateMembers creates a team membership for all provided user ids. +// +// Minimum server version: 5.2 +func (t *TeamService) CreateMembers(teamID string, userIDs []string, requestorID string) ([]*model.TeamMember, error) { + teamMembers, appErr := t.api.CreateTeamMembers(teamID, userIDs, requestorID) + + return teamMembers, normalizeAppErr(appErr) +} + +// DeleteMember deletes a team membership. +// +// Minimum server version: 5.2 +func (t *TeamService) DeleteMember(teamID, userID, requestorID string) error { + return normalizeAppErr(t.api.DeleteTeamMember(teamID, userID, requestorID)) +} + +// UpdateMemberRoles updates the role for a team membership. +// +// Minimum server version: 5.2 +func (t *TeamService) UpdateMemberRoles(teamID, userID, newRoles string) (*model.TeamMember, error) { + teamMember, appErr := t.api.UpdateTeamMemberRoles(teamID, userID, newRoles) + + return teamMember, normalizeAppErr(appErr) +} + +// GetStats gets a team's statistics +// +// Minimum server version: 5.8 +func (t *TeamService) GetStats(teamID string) (*model.TeamStats, error) { + teamStats, appErr := t.api.GetTeamStats(teamID) + + return teamStats, normalizeAppErr(appErr) +} diff --git a/server/public/pluginapi/team_test.go b/server/public/pluginapi/team_test.go new file mode 100644 index 00000000000..4dfe6d79ffb --- /dev/null +++ b/server/public/pluginapi/team_test.go @@ -0,0 +1,587 @@ +package pluginapi_test + +import ( + "bytes" + "io" + "net/http" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/mattermost/mattermost/server/public/model" + "github.com/mattermost/mattermost/server/public/plugin/plugintest" + "github.com/mattermost/mattermost/server/public/pluginapi" +) + +func TestCreateTeam(t *testing.T) { + t.Run("success", func(t *testing.T) { + api := &plugintest.API{} + defer api.AssertExpectations(t) + client := pluginapi.NewClient(api, &plugintest.Driver{}) + + api.On("CreateTeam", &model.Team{Name: "1"}).Return(&model.Team{Name: "1", Id: "2"}, nil) + + team := &model.Team{Name: "1"} + err := client.Team.Create(team) + require.NoError(t, err) + require.Equal(t, &model.Team{Name: "1", Id: "2"}, team) + }) + + t.Run("failure", func(t *testing.T) { + api := &plugintest.API{} + defer api.AssertExpectations(t) + client := pluginapi.NewClient(api, &plugintest.Driver{}) + + appErr := model.NewAppError("here", "id", nil, "an error occurred", http.StatusInternalServerError) + + api.On("CreateTeam", &model.Team{Name: "1"}).Return(nil, appErr) + + team := &model.Team{Name: "1"} + err := client.Team.Create(team) + require.Equal(t, appErr, err) + require.Equal(t, &model.Team{Name: "1"}, team) + }) +} + +func TestGetTeam(t *testing.T) { + t.Run("success", func(t *testing.T) { + api := &plugintest.API{} + defer api.AssertExpectations(t) + client := pluginapi.NewClient(api, &plugintest.Driver{}) + + api.On("GetTeam", "1").Return(&model.Team{Id: "2"}, nil) + + team, err := client.Team.Get("1") + require.NoError(t, err) + require.Equal(t, &model.Team{Id: "2"}, team) + }) + + t.Run("failure", func(t *testing.T) { + api := &plugintest.API{} + defer api.AssertExpectations(t) + + client := pluginapi.NewClient(api, &plugintest.Driver{}) + + appErr := model.NewAppError("here", "id", nil, "an error occurred", http.StatusInternalServerError) + + api.On("GetTeam", "1").Return(nil, appErr) + + team, err := client.Team.Get("1") + require.Equal(t, appErr, err) + require.Zero(t, team) + }) +} + +func TestGetTeamByName(t *testing.T) { + t.Run("success", func(t *testing.T) { + api := &plugintest.API{} + defer api.AssertExpectations(t) + client := pluginapi.NewClient(api, &plugintest.Driver{}) + + api.On("GetTeamByName", "1").Return(&model.Team{Id: "2"}, nil) + + team, err := client.Team.GetByName("1") + require.NoError(t, err) + require.Equal(t, &model.Team{Id: "2"}, team) + }) + + t.Run("failure", func(t *testing.T) { + api := &plugintest.API{} + defer api.AssertExpectations(t) + client := pluginapi.NewClient(api, &plugintest.Driver{}) + + appErr := model.NewAppError("here", "id", nil, "an error occurred", http.StatusInternalServerError) + + api.On("GetTeamByName", "1").Return(nil, appErr) + + team, err := client.Team.GetByName("1") + require.Equal(t, appErr, err) + require.Zero(t, team) + }) +} + +func TestUpdateTeam(t *testing.T) { + t.Run("success", func(t *testing.T) { + api := &plugintest.API{} + defer api.AssertExpectations(t) + client := pluginapi.NewClient(api, &plugintest.Driver{}) + + api.On("UpdateTeam", &model.Team{Name: "1"}).Return(&model.Team{Name: "1", Id: "2"}, nil) + + team := &model.Team{Name: "1"} + err := client.Team.Update(team) + require.NoError(t, err) + require.Equal(t, &model.Team{Name: "1", Id: "2"}, team) + }) + + t.Run("failure", func(t *testing.T) { + api := &plugintest.API{} + defer api.AssertExpectations(t) + client := pluginapi.NewClient(api, &plugintest.Driver{}) + + appErr := model.NewAppError("here", "id", nil, "an error occurred", http.StatusInternalServerError) + + api.On("UpdateTeam", &model.Team{Name: "1"}).Return(nil, appErr) + + team := &model.Team{Name: "1"} + err := client.Team.Update(team) + require.Equal(t, appErr, err) + require.Equal(t, &model.Team{Name: "1"}, team) + }) +} + +func TestListTeams(t *testing.T) { + t.Run("list all", func(t *testing.T) { + api := &plugintest.API{} + defer api.AssertExpectations(t) + client := pluginapi.NewClient(api, &plugintest.Driver{}) + + api.On("GetTeams").Return([]*model.Team{{Id: "1"}, {Id: "2"}}, nil) + + teams, err := client.Team.List() + require.NoError(t, err) + require.Equal(t, []*model.Team{{Id: "1"}, {Id: "2"}}, teams) + }) + + t.Run("list scoped to user", func(t *testing.T) { + api := &plugintest.API{} + defer api.AssertExpectations(t) + client := pluginapi.NewClient(api, &plugintest.Driver{}) + + api.On("GetTeamsForUser", "3").Return([]*model.Team{{Id: "1"}, {Id: "2"}}, nil) + + teams, err := client.Team.List(pluginapi.FilterTeamsByUser("3")) + require.NoError(t, err) + require.Equal(t, []*model.Team{{Id: "1"}, {Id: "2"}}, teams) + }) + + t.Run("failure", func(t *testing.T) { + api := &plugintest.API{} + defer api.AssertExpectations(t) + client := pluginapi.NewClient(api, &plugintest.Driver{}) + + appErr := model.NewAppError("here", "id", nil, "an error occurred", http.StatusInternalServerError) + + api.On("GetTeams").Return(nil, appErr) + + teams, err := client.Team.List() + require.Equal(t, appErr, err) + require.Len(t, teams, 0) + }) +} + +func TestSearchTeams(t *testing.T) { + t.Run("success", func(t *testing.T) { + api := &plugintest.API{} + defer api.AssertExpectations(t) + client := pluginapi.NewClient(api, &plugintest.Driver{}) + + api.On("SearchTeams", "1").Return([]*model.Team{{Id: "1"}, {Id: "2"}}, nil) + + teams, err := client.Team.Search("1") + require.NoError(t, err) + require.Equal(t, []*model.Team{{Id: "1"}, {Id: "2"}}, teams) + }) + + t.Run("failure", func(t *testing.T) { + api := &plugintest.API{} + defer api.AssertExpectations(t) + client := pluginapi.NewClient(api, &plugintest.Driver{}) + + appErr := model.NewAppError("here", "id", nil, "an error occurred", http.StatusInternalServerError) + + api.On("SearchTeams", "1").Return(nil, appErr) + + teams, err := client.Team.Search("1") + require.Equal(t, appErr, err) + require.Zero(t, teams) + }) +} + +func TestDeleteTeam(t *testing.T) { + t.Run("success", func(t *testing.T) { + api := &plugintest.API{} + defer api.AssertExpectations(t) + client := pluginapi.NewClient(api, &plugintest.Driver{}) + + api.On("DeleteTeam", "1").Return(nil) + + err := client.Team.Delete("1") + require.NoError(t, err) + }) + + t.Run("failure", func(t *testing.T) { + api := &plugintest.API{} + defer api.AssertExpectations(t) + client := pluginapi.NewClient(api, &plugintest.Driver{}) + + appErr := model.NewAppError("here", "id", nil, "an error occurred", http.StatusInternalServerError) + + api.On("DeleteTeam", "1").Return(appErr) + + err := client.Team.Delete("1") + require.Equal(t, appErr, err) + }) +} + +func TestGetTeamIcon(t *testing.T) { + t.Run("success", func(t *testing.T) { + api := &plugintest.API{} + defer api.AssertExpectations(t) + client := pluginapi.NewClient(api, &plugintest.Driver{}) + + api.On("GetTeamIcon", "1").Return([]byte{2}, nil) + + content, err := client.Team.GetIcon("1") + require.NoError(t, err) + contentBytes, err := io.ReadAll(content) + require.NoError(t, err) + require.Equal(t, []byte{2}, contentBytes) + }) + + t.Run("failure", func(t *testing.T) { + api := &plugintest.API{} + defer api.AssertExpectations(t) + client := pluginapi.NewClient(api, &plugintest.Driver{}) + + appErr := model.NewAppError("here", "id", nil, "an error occurred", http.StatusInternalServerError) + + api.On("GetTeamIcon", "1").Return(nil, appErr) + + content, err := client.Team.GetIcon("1") + require.Equal(t, appErr, err) + require.Zero(t, content) + }) +} + +func TestSetIcon(t *testing.T) { + t.Run("success", func(t *testing.T) { + api := &plugintest.API{} + defer api.AssertExpectations(t) + client := pluginapi.NewClient(api, &plugintest.Driver{}) + + api.On("SetTeamIcon", "1", []byte{2}).Return(nil) + + err := client.Team.SetIcon("1", bytes.NewReader([]byte{2})) + require.NoError(t, err) + }) + + t.Run("failure", func(t *testing.T) { + api := &plugintest.API{} + defer api.AssertExpectations(t) + client := pluginapi.NewClient(api, &plugintest.Driver{}) + + appErr := model.NewAppError("here", "id", nil, "an error occurred", http.StatusInternalServerError) + + api.On("SetTeamIcon", "1", []byte{2}).Return(appErr) + + err := client.Team.SetIcon("1", bytes.NewReader([]byte{2})) + require.Equal(t, appErr, err) + }) +} + +func TestDeleteIcon(t *testing.T) { + t.Run("success", func(t *testing.T) { + api := &plugintest.API{} + defer api.AssertExpectations(t) + client := pluginapi.NewClient(api, &plugintest.Driver{}) + + api.On("RemoveTeamIcon", "1").Return(nil) + + err := client.Team.DeleteIcon("1") + require.NoError(t, err) + }) + + t.Run("failure", func(t *testing.T) { + api := &plugintest.API{} + defer api.AssertExpectations(t) + client := pluginapi.NewClient(api, &plugintest.Driver{}) + + appErr := model.NewAppError("here", "id", nil, "an error occurred", http.StatusInternalServerError) + + api.On("RemoveTeamIcon", "1").Return(appErr) + + err := client.Team.DeleteIcon("1") + require.Equal(t, appErr, err) + }) +} + +func TestGetTeamUsers(t *testing.T) { + t.Run("success", func(t *testing.T) { + api := &plugintest.API{} + defer api.AssertExpectations(t) + client := pluginapi.NewClient(api, &plugintest.Driver{}) + + api.On("GetUsersInTeam", "1", 2, 3).Return([]*model.User{{Id: "1"}, {Id: "2"}}, nil) + + users, err := client.Team.ListUsers("1", 2, 3) + require.NoError(t, err) + require.Equal(t, []*model.User{{Id: "1"}, {Id: "2"}}, users) + }) + + t.Run("failure", func(t *testing.T) { + api := &plugintest.API{} + defer api.AssertExpectations(t) + client := pluginapi.NewClient(api, &plugintest.Driver{}) + + appErr := model.NewAppError("here", "id", nil, "an error occurred", http.StatusInternalServerError) + + api.On("GetUsersInTeam", "1", 2, 3).Return(nil, appErr) + + users, err := client.Team.ListUsers("1", 2, 3) + require.Equal(t, appErr, err) + require.Len(t, users, 0) + }) +} + +func TestGetTeamUnreads(t *testing.T) { + t.Run("success", func(t *testing.T) { + api := &plugintest.API{} + defer api.AssertExpectations(t) + + client := pluginapi.NewClient(api, &plugintest.Driver{}) + + api.On("GetTeamsUnreadForUser", "1").Return([]*model.TeamUnread{{TeamId: "1"}, {TeamId: "2"}}, nil) + + unreads, err := client.Team.ListUnreadForUser("1") + require.NoError(t, err) + require.Equal(t, []*model.TeamUnread{{TeamId: "1"}, {TeamId: "2"}}, unreads) + }) + + t.Run("failure", func(t *testing.T) { + api := &plugintest.API{} + defer api.AssertExpectations(t) + client := pluginapi.NewClient(api, &plugintest.Driver{}) + + appErr := model.NewAppError("here", "id", nil, "an error occurred", http.StatusInternalServerError) + + api.On("GetTeamsUnreadForUser", "1").Return(nil, appErr) + + unreads, err := client.Team.ListUnreadForUser("1") + require.Equal(t, appErr, err) + require.Len(t, unreads, 0) + }) +} + +func TestCreateTeamMember(t *testing.T) { + t.Run("success", func(t *testing.T) { + api := &plugintest.API{} + defer api.AssertExpectations(t) + client := pluginapi.NewClient(api, &plugintest.Driver{}) + + api.On("CreateTeamMember", "1", "2").Return(&model.TeamMember{TeamId: "3"}, nil) + + member, err := client.Team.CreateMember("1", "2") + require.NoError(t, err) + require.Equal(t, &model.TeamMember{TeamId: "3"}, member) + }) + + t.Run("failure", func(t *testing.T) { + api := &plugintest.API{} + defer api.AssertExpectations(t) + + client := pluginapi.NewClient(api, &plugintest.Driver{}) + + appErr := model.NewAppError("here", "id", nil, "an error occurred", http.StatusInternalServerError) + + api.On("CreateTeamMember", "1", "2").Return(nil, appErr) + + member, err := client.Team.CreateMember("1", "2") + require.Equal(t, appErr, err) + require.Zero(t, member) + }) +} + +func TestCreateTeamMembers(t *testing.T) { + t.Run("success", func(t *testing.T) { + api := &plugintest.API{} + defer api.AssertExpectations(t) + client := pluginapi.NewClient(api, &plugintest.Driver{}) + + api.On("CreateTeamMembers", "1", []string{"2"}, "3").Return([]*model.TeamMember{{TeamId: "4"}, {TeamId: "5"}}, nil) + + members, err := client.Team.CreateMembers("1", []string{"2"}, "3") + require.NoError(t, err) + require.Equal(t, []*model.TeamMember{{TeamId: "4"}, {TeamId: "5"}}, members) + }) + + t.Run("failure", func(t *testing.T) { + api := &plugintest.API{} + defer api.AssertExpectations(t) + client := pluginapi.NewClient(api, &plugintest.Driver{}) + + appErr := model.NewAppError("here", "id", nil, "an error occurred", http.StatusInternalServerError) + + api.On("CreateTeamMembers", "1", []string{"2"}, "3").Return(nil, appErr) + + members, err := client.Team.CreateMembers("1", []string{"2"}, "3") + require.Equal(t, appErr, err) + require.Len(t, members, 0) + }) +} + +func TestGetTeamMember(t *testing.T) { + t.Run("success", func(t *testing.T) { + api := &plugintest.API{} + defer api.AssertExpectations(t) + client := pluginapi.NewClient(api, &plugintest.Driver{}) + + api.On("GetTeamMember", "1", "2").Return(&model.TeamMember{TeamId: "3"}, nil) + + member, err := client.Team.GetMember("1", "2") + require.NoError(t, err) + require.Equal(t, &model.TeamMember{TeamId: "3"}, member) + }) + + t.Run("failure", func(t *testing.T) { + api := &plugintest.API{} + defer api.AssertExpectations(t) + client := pluginapi.NewClient(api, &plugintest.Driver{}) + + appErr := model.NewAppError("here", "id", nil, "an error occurred", http.StatusInternalServerError) + + api.On("GetTeamMember", "1", "2").Return(nil, appErr) + + member, err := client.Team.GetMember("1", "2") + require.Equal(t, appErr, err) + require.Zero(t, member) + }) +} + +func TestGetTeamMembers(t *testing.T) { + t.Run("success", func(t *testing.T) { + api := &plugintest.API{} + defer api.AssertExpectations(t) + client := pluginapi.NewClient(api, &plugintest.Driver{}) + + api.On("GetTeamMembers", "1", 2, 3).Return([]*model.TeamMember{{TeamId: "4"}, {TeamId: "5"}}, nil) + + members, err := client.Team.ListMembers("1", 2, 3) + require.NoError(t, err) + require.Equal(t, []*model.TeamMember{{TeamId: "4"}, {TeamId: "5"}}, members) + }) + + t.Run("failure", func(t *testing.T) { + api := &plugintest.API{} + defer api.AssertExpectations(t) + client := pluginapi.NewClient(api, &plugintest.Driver{}) + + appErr := model.NewAppError("here", "id", nil, "an error occurred", http.StatusInternalServerError) + + api.On("GetTeamMembers", "1", 2, 3).Return(nil, appErr) + + members, err := client.Team.ListMembers("1", 2, 3) + require.Equal(t, appErr, err) + require.Len(t, members, 0) + }) +} + +func TestGetUserMemberships(t *testing.T) { + t.Run("success", func(t *testing.T) { + api := &plugintest.API{} + defer api.AssertExpectations(t) + client := pluginapi.NewClient(api, &plugintest.Driver{}) + + api.On("GetTeamMembersForUser", "1", 2, 3).Return([]*model.TeamMember{{TeamId: "4"}, {TeamId: "5"}}, nil) + + members, err := client.Team.ListMembersForUser("1", 2, 3) + require.NoError(t, err) + require.Equal(t, []*model.TeamMember{{TeamId: "4"}, {TeamId: "5"}}, members) + }) + + t.Run("failure", func(t *testing.T) { + api := &plugintest.API{} + defer api.AssertExpectations(t) + client := pluginapi.NewClient(api, &plugintest.Driver{}) + + appErr := model.NewAppError("here", "id", nil, "an error occurred", http.StatusInternalServerError) + + api.On("GetTeamMembersForUser", "1", 2, 3).Return(nil, appErr) + + members, err := client.Team.ListMembersForUser("1", 2, 3) + require.Equal(t, appErr, err) + require.Len(t, members, 0) + }) +} + +func TestUpdateTeamMemberRoles(t *testing.T) { + t.Run("success", func(t *testing.T) { + api := &plugintest.API{} + defer api.AssertExpectations(t) + client := pluginapi.NewClient(api, &plugintest.Driver{}) + + api.On("UpdateTeamMemberRoles", "1", "2", "3").Return(&model.TeamMember{TeamId: "3"}, nil) + + membership, err := client.Team.UpdateMemberRoles("1", "2", "3") + require.NoError(t, err) + require.Equal(t, &model.TeamMember{TeamId: "3"}, membership) + }) + + t.Run("failure", func(t *testing.T) { + api := &plugintest.API{} + defer api.AssertExpectations(t) + client := pluginapi.NewClient(api, &plugintest.Driver{}) + + appErr := model.NewAppError("here", "id", nil, "an error occurred", http.StatusInternalServerError) + + api.On("UpdateTeamMemberRoles", "1", "2", "3").Return(nil, appErr) + + membership, err := client.Team.UpdateMemberRoles("1", "2", "3") + require.Equal(t, appErr, err) + require.Zero(t, membership) + }) +} + +func TestDeleteTeamMember(t *testing.T) { + t.Run("success", func(t *testing.T) { + api := &plugintest.API{} + defer api.AssertExpectations(t) + client := pluginapi.NewClient(api, &plugintest.Driver{}) + + api.On("DeleteTeamMember", "1", "2", "3").Return(nil) + + err := client.Team.DeleteMember("1", "2", "3") + require.NoError(t, err) + }) + + t.Run("failure", func(t *testing.T) { + api := &plugintest.API{} + defer api.AssertExpectations(t) + client := pluginapi.NewClient(api, &plugintest.Driver{}) + + appErr := model.NewAppError("here", "id", nil, "an error occurred", http.StatusInternalServerError) + + api.On("DeleteTeamMember", "1", "2", "3").Return(appErr) + + err := client.Team.DeleteMember("1", "2", "3") + require.Equal(t, appErr, err) + }) +} + +func TestGetTeamStats(t *testing.T) { + t.Run("success", func(t *testing.T) { + api := &plugintest.API{} + defer api.AssertExpectations(t) + client := pluginapi.NewClient(api, &plugintest.Driver{}) + + api.On("GetTeamStats", "1").Return(&model.TeamStats{TeamId: "3"}, nil) + + stats, err := client.Team.GetStats("1") + require.NoError(t, err) + require.Equal(t, &model.TeamStats{TeamId: "3"}, stats) + }) + + t.Run("failure", func(t *testing.T) { + api := &plugintest.API{} + defer api.AssertExpectations(t) + client := pluginapi.NewClient(api, &plugintest.Driver{}) + + appErr := model.NewAppError("here", "id", nil, "an error occurred", http.StatusInternalServerError) + + api.On("GetTeamStats", "1").Return(nil, appErr) + + stats, err := client.Team.GetStats("1") + require.Equal(t, appErr, err) + require.Zero(t, stats) + }) +} diff --git a/server/public/pluginapi/user.go b/server/public/pluginapi/user.go new file mode 100644 index 00000000000..bb6252dd6af --- /dev/null +++ b/server/public/pluginapi/user.go @@ -0,0 +1,246 @@ +package pluginapi + +import ( + "bytes" + "io" + + "github.com/mattermost/mattermost/server/public/model" + "github.com/mattermost/mattermost/server/public/plugin" +) + +// UserService exposes methods to manipulate users. +type UserService struct { + api plugin.API +} + +// Get gets a user. +// +// Minimum server version: 5.2 +func (u *UserService) Get(userID string) (*model.User, error) { + user, appErr := u.api.GetUser(userID) + + return user, normalizeAppErr(appErr) +} + +// GetByEmail gets a user by their email address. +// +// Minimum server version: 5.2 +func (u *UserService) GetByEmail(email string) (*model.User, error) { + user, appErr := u.api.GetUserByEmail(email) + + return user, normalizeAppErr(appErr) +} + +// GetByUsername gets a user by their username. +// +// Minimum server version: 5.2 +func (u *UserService) GetByUsername(username string) (*model.User, error) { + user, appErr := u.api.GetUserByUsername(username) + + return user, normalizeAppErr(appErr) +} + +// List a list of users based on search options. +// +// Minimum server version: 5.10 +func (u *UserService) List(options *model.UserGetOptions) ([]*model.User, error) { + users, appErr := u.api.GetUsers(options) + + return users, normalizeAppErr(appErr) +} + +// ListByUsernames gets users by their usernames. +// +// Minimum server version: 5.6 +func (u *UserService) ListByUsernames(usernames []string) ([]*model.User, error) { + users, appErr := u.api.GetUsersByUsernames(usernames) + + return users, normalizeAppErr(appErr) +} + +// ListInChannel returns a page of users in a channel. Page counting starts at 0. +// The sortBy parameter can be: "username" or "status". +// +// Minimum server version: 5.6 +func (u *UserService) ListInChannel(channelID, sortBy string, page, perPage int) ([]*model.User, error) { + users, appErr := u.api.GetUsersInChannel(channelID, sortBy, page, perPage) + + return users, normalizeAppErr(appErr) +} + +// ListInTeam gets users in team. +// +// Minimum server version: 5.6 +func (u *UserService) ListInTeam(teamID string, page, perPage int) ([]*model.User, error) { + users, appErr := u.api.GetUsersInTeam(teamID, page, perPage) + + return users, normalizeAppErr(appErr) +} + +// Search returns a list of users based on some search criteria. +// +// Minimum server version: 5.6 +func (u *UserService) Search(search *model.UserSearch) ([]*model.User, error) { + users, appErr := u.api.SearchUsers(search) + + return users, normalizeAppErr(appErr) +} + +// Create creates a user. +// +// Minimum server version: 5.2 +func (u *UserService) Create(user *model.User) error { + createdUser, appErr := u.api.CreateUser(user) + if appErr != nil { + return normalizeAppErr(appErr) + } + + *user = *createdUser + + return nil +} + +// Update updates a user. +// +// Minimum server version: 5.2 +func (u *UserService) Update(user *model.User) error { + updatedUser, appErr := u.api.UpdateUser(user) + if appErr != nil { + return normalizeAppErr(appErr) + } + + *user = *updatedUser + + return nil +} + +// Delete deletes a user. +// +// Minimum server version: 5.2 +func (u *UserService) Delete(userID string) error { + appErr := u.api.DeleteUser(userID) + + return normalizeAppErr(appErr) +} + +// GetStatus will get a user's status. +// +// Minimum server version: 5.2 +func (u *UserService) GetStatus(userID string) (*model.Status, error) { + status, appErr := u.api.GetUserStatus(userID) + + return status, normalizeAppErr(appErr) +} + +// ListStatusesByIDs will return a list of user statuses based on the provided slice of user IDs. +// +// Minimum server version: 5.2 +func (u *UserService) ListStatusesByIDs(userIDs []string) ([]*model.Status, error) { + statuses, appErr := u.api.GetUserStatusesByIds(userIDs) + + return statuses, normalizeAppErr(appErr) +} + +// UpdateStatus will set a user's status until the user, or another integration/plugin, sets it back to online. +// The status parameter can be: "online", "away", "dnd", or "offline". +// +// Minimum server version: 5.2 +func (u *UserService) UpdateStatus(userID, status string) (*model.Status, error) { + rStatus, appErr := u.api.UpdateUserStatus(userID, status) + + return rStatus, normalizeAppErr(appErr) +} + +// UpdateActive deactivates or reactivates an user. +// +// Minimum server version: 5.8 +func (u *UserService) UpdateActive(userID string, active bool) error { + appErr := u.api.UpdateUserActive(userID, active) + + return normalizeAppErr(appErr) +} + +// GetProfileImage gets user's profile image. +// +// Minimum server version: 5.6 +func (u *UserService) GetProfileImage(userID string) (io.Reader, error) { + contentBytes, appErr := u.api.GetProfileImage(userID) + if appErr != nil { + return nil, normalizeAppErr(appErr) + } + + return bytes.NewReader(contentBytes), nil +} + +// SetProfileImage sets a user's profile image. +// +// Minimum server version: 5.6 +func (u *UserService) SetProfileImage(userID string, content io.Reader) error { + contentBytes, err := io.ReadAll(content) + if err != nil { + return err + } + + return normalizeAppErr(u.api.SetProfileImage(userID, contentBytes)) +} + +// HasPermissionTo check if the user has the permission at system scope. +// +// Minimum server version: 5.3 +func (u *UserService) HasPermissionTo(userID string, permission *model.Permission) bool { + return u.api.HasPermissionTo(userID, permission) +} + +// HasPermissionToTeam check if the user has the permission at team scope. +// +// Minimum server version: 5.3 +func (u *UserService) HasPermissionToTeam(userID, teamID string, permission *model.Permission) bool { + return u.api.HasPermissionToTeam(userID, teamID, permission) +} + +// HasPermissionToChannel check if the user has the permission at channel scope. +// +// Minimum server version: 5.3 +func (u *UserService) HasPermissionToChannel(userID, channelID string, permission *model.Permission) bool { + return u.api.HasPermissionToChannel(userID, channelID, permission) +} + +// RolesGrantPermission check if the specified roles grant the specified permission +// +// Minimum server version: 6.3 +func (u *UserService) RolesGrantPermission(roleNames []string, permissionID string) bool { + return u.api.RolesGrantPermission(roleNames, permissionID) +} + +// GetLDAPAttributes will return LDAP attributes for a user. +// The attributes parameter should be a list of attributes to pull. +// Returns a map with attribute names as keys and the user's attributes as values. +// Requires an enterprise license, LDAP to be configured and for the user to use LDAP as an authentication method. +// +// Minimum server version: 5.3 +func (u *UserService) GetLDAPAttributes(userID string, attributes []string) (map[string]string, error) { + ldapUserAttributes, appErr := u.api.GetLDAPUserAttributes(userID, attributes) + + return ldapUserAttributes, normalizeAppErr(appErr) +} + +// CreateAccessToken creates a new access token. +// +// Minimum server version: 5.38 +func (u *UserService) CreateAccessToken(userID, description string) (*model.UserAccessToken, error) { + token := &model.UserAccessToken{ + UserId: userID, + Description: description, + } + + createdToken, appErr := u.api.CreateUserAccessToken(token) + + return createdToken, normalizeAppErr(appErr) +} + +// RevokeAccessToken revokes an existing access token. +// +// Minimum server version: 5.38 +func (u *UserService) RevokeAccessToken(tokenID string) error { + return normalizeAppErr(u.api.RevokeUserAccessToken(tokenID)) +} diff --git a/server/public/pluginapi/user_test.go b/server/public/pluginapi/user_test.go new file mode 100644 index 00000000000..beb5138a157 --- /dev/null +++ b/server/public/pluginapi/user_test.go @@ -0,0 +1,257 @@ +package pluginapi_test + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/mattermost/mattermost/server/public/model" + "github.com/mattermost/mattermost/server/public/plugin/plugintest" + "github.com/mattermost/mattermost/server/public/pluginapi" +) + +func TestCreateUser(t *testing.T) { + t.Run("success", func(t *testing.T) { + api := &plugintest.API{} + defer api.AssertExpectations(t) + client := pluginapi.NewClient(api, &plugintest.Driver{}) + + expectedUser := &model.User{ + Username: "test", + } + api.On("CreateUser", expectedUser).Return(expectedUser, nil) + + err := client.User.Create(expectedUser) + require.NoError(t, err) + }) + + t.Run("failure", func(t *testing.T) { + api := &plugintest.API{} + defer api.AssertExpectations(t) + client := pluginapi.NewClient(api, &plugintest.Driver{}) + + expectedUser := &model.User{ + Username: "test", + } + api.On("CreateUser", expectedUser).Return(nil, newAppError()) + + err := client.User.Create(expectedUser) + require.EqualError(t, err, "here: id, an error occurred") + }) +} + +func TestDeleteUser(t *testing.T) { + t.Run("success", func(t *testing.T) { + api := &plugintest.API{} + defer api.AssertExpectations(t) + client := pluginapi.NewClient(api, &plugintest.Driver{}) + + expectedUserID := model.NewId() + api.On("DeleteUser", expectedUserID).Return(nil) + + err := client.User.Delete(expectedUserID) + require.NoError(t, err) + }) + + t.Run("failure", func(t *testing.T) { + api := &plugintest.API{} + defer api.AssertExpectations(t) + client := pluginapi.NewClient(api, &plugintest.Driver{}) + + expectedUserID := model.NewId() + api.On("DeleteUser", expectedUserID).Return(newAppError()) + + err := client.User.Delete(expectedUserID) + require.EqualError(t, err, "here: id, an error occurred") + }) +} + +func TestGetUsers(t *testing.T) { + t.Run("success", func(t *testing.T) { + api := &plugintest.API{} + defer api.AssertExpectations(t) + client := pluginapi.NewClient(api, &plugintest.Driver{}) + + options := &model.UserGetOptions{} + expectedUsers := []*model.User{{Username: "test"}} + api.On("GetUsers", options).Return(expectedUsers, nil) + + actualUsers, err := client.User.List(options) + require.NoError(t, err) + assert.Equal(t, expectedUsers, actualUsers) + }) + + t.Run("failure", func(t *testing.T) { + api := &plugintest.API{} + defer api.AssertExpectations(t) + client := pluginapi.NewClient(api, &plugintest.Driver{}) + + options := &model.UserGetOptions{} + api.On("GetUsers", options).Return(nil, newAppError()) + + actualUsers, err := client.User.List(options) + require.EqualError(t, err, "here: id, an error occurred") + assert.Nil(t, actualUsers) + }) +} + +func TestGetUser(t *testing.T) { + t.Run("success", func(t *testing.T) { + api := &plugintest.API{} + defer api.AssertExpectations(t) + client := pluginapi.NewClient(api, &plugintest.Driver{}) + + userID := "id" + expectedUser := &model.User{Id: userID, Username: "test"} + api.On("GetUser", userID).Return(expectedUser, nil) + + actualUser, err := client.User.Get(userID) + require.NoError(t, err) + assert.Equal(t, expectedUser, actualUser) + }) + + t.Run("failure", func(t *testing.T) { + api := &plugintest.API{} + defer api.AssertExpectations(t) + client := pluginapi.NewClient(api, &plugintest.Driver{}) + + userID := "id" + api.On("GetUser", userID).Return(nil, newAppError()) + + actualUser, err := client.User.Get(userID) + require.EqualError(t, err, "here: id, an error occurred") + assert.Nil(t, actualUser) + }) +} + +func TestGetUserByEmail(t *testing.T) { + t.Run("success", func(t *testing.T) { + api := &plugintest.API{} + defer api.AssertExpectations(t) + client := pluginapi.NewClient(api, &plugintest.Driver{}) + + email := "test@example.com" + expectedUser := &model.User{Email: email, Username: "test"} + api.On("GetUserByEmail", email).Return(expectedUser, nil) + + actualUser, err := client.User.GetByEmail(email) + require.NoError(t, err) + assert.Equal(t, expectedUser, actualUser) + }) + + t.Run("failure", func(t *testing.T) { + api := &plugintest.API{} + defer api.AssertExpectations(t) + client := pluginapi.NewClient(api, &plugintest.Driver{}) + + email := "test@example.com" + api.On("GetUserByEmail", email).Return(nil, newAppError()) + + actualUser, err := client.User.GetByEmail(email) + require.EqualError(t, err, "here: id, an error occurred") + assert.Nil(t, actualUser) + }) +} + +func TestGetUserByUsername(t *testing.T) { + t.Run("success", func(t *testing.T) { + api := &plugintest.API{} + defer api.AssertExpectations(t) + client := pluginapi.NewClient(api, &plugintest.Driver{}) + + username := "test" + expectedUser := &model.User{Username: username} + api.On("GetUserByUsername", username).Return(expectedUser, nil) + + actualUser, err := client.User.GetByUsername(username) + require.NoError(t, err) + assert.Equal(t, expectedUser, actualUser) + }) + + t.Run("failure", func(t *testing.T) { + api := &plugintest.API{} + defer api.AssertExpectations(t) + client := pluginapi.NewClient(api, &plugintest.Driver{}) + + username := "test" + api.On("GetUserByUsername", username).Return(nil, newAppError()) + + actualUser, err := client.User.GetByUsername(username) + require.EqualError(t, err, "here: id, an error occurred") + assert.Nil(t, actualUser) + }) +} + +func TestGetUsersByUsernames(t *testing.T) { + t.Run("success", func(t *testing.T) { + api := &plugintest.API{} + defer api.AssertExpectations(t) + client := pluginapi.NewClient(api, &plugintest.Driver{}) + + usernames := []string{"test1", "test2"} + expectedUsers := []*model.User{{Username: "test1"}, {Username: "test2"}} + api.On("GetUsersByUsernames", usernames).Return(expectedUsers, nil) + + actualUsers, err := client.User.ListByUsernames(usernames) + require.NoError(t, err) + assert.Equal(t, expectedUsers, actualUsers) + }) + + t.Run("failure", func(t *testing.T) { + api := &plugintest.API{} + defer api.AssertExpectations(t) + client := pluginapi.NewClient(api, &plugintest.Driver{}) + + usernames := []string{"test1", "test2"} + api.On("GetUsersByUsernames", usernames).Return(nil, newAppError()) + + actualUsers, err := client.User.ListByUsernames(usernames) + require.EqualError(t, err, "here: id, an error occurred") + assert.Nil(t, actualUsers) + }) +} + +func TestGetUsersInTeam(t *testing.T) { + t.Run("success", func(t *testing.T) { + api := &plugintest.API{} + defer api.AssertExpectations(t) + client := pluginapi.NewClient(api, &plugintest.Driver{}) + + teamID := "team_id" + page := 1 + perPage := 10 + expectedUsers := []*model.User{{Username: "test1"}, {Username: "test2"}} + api.On("GetUsersInTeam", teamID, page, perPage).Return(expectedUsers, nil) + + actualUsers, err := client.User.ListInTeam(teamID, page, perPage) + require.NoError(t, err) + assert.Equal(t, expectedUsers, actualUsers) + }) + + t.Run("failure", func(t *testing.T) { + api := &plugintest.API{} + defer api.AssertExpectations(t) + client := pluginapi.NewClient(api, &plugintest.Driver{}) + + teamID := "team_id" + page := 1 + perPage := 10 + api.On("GetUsersInTeam", teamID, page, perPage).Return(nil, newAppError()) + + actualUsers, err := client.User.ListInTeam(teamID, page, perPage) + require.EqualError(t, err, "here: id, an error occurred") + assert.Nil(t, actualUsers) + }) +} + +func TestHasTeamUserPermission(t *testing.T) { + api := &plugintest.API{} + defer api.AssertExpectations(t) + client := pluginapi.NewClient(api, &plugintest.Driver{}) + + api.On("HasPermissionToTeam", "1", "2", &model.Permission{Id: "3"}).Return(true) + + ok := client.User.HasPermissionToTeam("1", "2", &model.Permission{Id: "3"}) + require.True(t, ok) +} diff --git a/server/public/pluginapi/utils.go b/server/public/pluginapi/utils.go new file mode 100644 index 00000000000..d1ff68ed342 --- /dev/null +++ b/server/public/pluginapi/utils.go @@ -0,0 +1,40 @@ +package pluginapi + +import ( + "time" +) + +func stringInSlice(a string, slice []string) bool { + for _, b := range slice { + if b == a { + return true + } + } + + return false +} + +var backoffTimeouts = []time.Duration{ + 50 * time.Millisecond, + 100 * time.Millisecond, + 200 * time.Millisecond, + 200 * time.Millisecond, + 400 * time.Millisecond, + 400 * time.Millisecond, +} + +// progressiveRetry executes a BackoffOperation and waits an increasing time before retrying the operation. +func progressiveRetry(operation func() error) error { + var err error + + for attempts := 0; attempts < len(backoffTimeouts); attempts++ { + err = operation() + if err == nil { + return nil + } + + time.Sleep(backoffTimeouts[attempts]) + } + + return err +} diff --git a/server/public/pluginapi/utils_test.go b/server/public/pluginapi/utils_test.go new file mode 100644 index 00000000000..7ee8d78656e --- /dev/null +++ b/server/public/pluginapi/utils_test.go @@ -0,0 +1,64 @@ +package pluginapi + +import ( + "testing" + + "github.com/pkg/errors" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestProgressiveRetry(t *testing.T) { + var retries int + + type args struct { + operation func() error + } + tests := []struct { + name string + args args + wantErr bool + expectedRetries int + }{ + { + name: "Should fail and return error", + args: args{ + operation: func() error { + retries++ + return errors.New("Operation Failed") + }, + }, + wantErr: true, + expectedRetries: 6, + }, + { + name: "Should succeed after two retries", + args: args{ + operation: func() error { + retries++ + if retries == 2 { + return nil + } + + return errors.New("Operation Failed") + }, + }, + wantErr: false, + expectedRetries: 2, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + retries = 0 + + err := progressiveRetry(tt.args.operation) + if tt.wantErr { + require.Error(t, err) + } else { + require.NoError(t, err) + } + + assert.Equal(t, tt.expectedRetries, retries) + }) + } +} diff --git a/server/platform/shared/driver/conn.go b/server/public/shared/driver/conn.go similarity index 100% rename from server/platform/shared/driver/conn.go rename to server/public/shared/driver/conn.go diff --git a/server/platform/shared/driver/driver.go b/server/public/shared/driver/driver.go similarity index 100% rename from server/platform/shared/driver/driver.go rename to server/public/shared/driver/driver.go diff --git a/server/platform/shared/driver/objects.go b/server/public/shared/driver/objects.go similarity index 100% rename from server/platform/shared/driver/objects.go rename to server/public/shared/driver/objects.go diff --git a/server/public/shared/i18n/i18n.go b/server/public/shared/i18n/i18n.go index 67fbb5a003c..3cdf1c379b7 100644 --- a/server/public/shared/i18n/i18n.go +++ b/server/public/shared/i18n/i18n.go @@ -33,6 +33,32 @@ var T TranslateFunc var TDefault TranslateFunc var locales = make(map[string]string) + +// supportedLocales is a hard-coded list of locales considered ready for production use. It must +// be kept in sync with ../../../../webapp/channels/src/i18n/i18n.jsx. +var supportedLocales = []string{ + "de", + "en", + "en-AU", + "es", + "fr", + "it", + "hu", + "nl", + "pl", + "pt-BR", + "ro", + "sv", + "tr", + "bg", + "ru", + "uk", + "fa", + "ko", + "zh-CN", + "zh-TW", + "ja", +} var defaultServerLocale string var defaultClientLocale string @@ -67,7 +93,13 @@ func initTranslationsWithDir(dir string) error { for _, f := range files { if filepath.Ext(f.Name()) == ".json" { filename := f.Name() - locales[strings.Split(filename, ".")[0]] = filepath.Join(dir, filename) + + locale := strings.Split(filename, ".")[0] + if !isSupportedLocale(locale) { + continue + } + + locales[locale] = filepath.Join(dir, filename) if err := i18n.LoadTranslationFile(filepath.Join(dir, filename)); err != nil { return err @@ -89,8 +121,13 @@ func GetTranslationFuncForDir(dir string) (TranslationFuncByLocal, error) { continue } + locale := strings.Split(f.Name(), ".")[0] + if !isSupportedLocale(locale) { + continue + } + filename := f.Name() - availableLocals[strings.Split(filename, ".")[0]] = filepath.Join(dir, filename) + availableLocals[locale] = filepath.Join(dir, filename) if err := bundle.LoadTranslationFile(filepath.Join(dir, filename)); err != nil { return nil, err } @@ -116,7 +153,12 @@ func GetTranslationFuncForDir(dir string) (TranslationFuncByLocal, error) { func getTranslationsBySystemLocale() (TranslateFunc, error) { locale := defaultServerLocale if _, ok := locales[locale]; !ok { - mlog.Warn("Failed to load system translations for", mlog.String("locale", locale), mlog.String("attempting to fall back to default locale", defaultLocale)) + mlog.Warn("Failed to load system translations for selected locale, attempting to fall back to default", mlog.String("locale", locale), mlog.String("default_locale", defaultLocale)) + locale = defaultLocale + } + + if !isSupportedLocale(locale) { + mlog.Warn("Selected locale is unsupported, attempting to fall back to default", mlog.String("locale", locale), mlog.String("default_locale", defaultLocale)) locale = defaultLocale } @@ -222,3 +264,13 @@ func IdentityTfunc() TranslateFunc { return translationID } } + +func isSupportedLocale(locale string) bool { + for _, supportedLocale := range supportedLocales { + if locale == supportedLocale { + return true + } + } + + return false +} diff --git a/server/public/shared/i18n/i18n_test.go b/server/public/shared/i18n/i18n_test.go index 9f823387234..8b4000acf9d 100644 --- a/server/public/shared/i18n/i18n_test.go +++ b/server/public/shared/i18n/i18n_test.go @@ -4,12 +4,17 @@ package i18n import ( + "fmt" + "os" + "path/filepath" "testing" "github.com/mattermost/go-i18n/i18n/bundle" "github.com/mattermost/go-i18n/i18n/language" "github.com/mattermost/go-i18n/i18n/translation" + "github.com/mattermost/mattermost/server/public/utils" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) var htmlTestTranslationBundle *bundle.Bundle @@ -67,3 +72,155 @@ func TestEscapeForHTML(t *testing.T) { }) } } + +func TestInitTranslationsWithDir(t *testing.T) { + i18nDir, found := utils.FindDir("server/i18n") + require.True(t, found, "unable to find i18n dir") + + setup := func(t *testing.T, localesToCopy map[string]string) string { + tempDir, err := os.MkdirTemp(os.TempDir(), "TestGetTranslationFuncForDir") + require.NoError(t, err, "unable to create temporary directory") + + t.Cleanup(func() { + err = os.RemoveAll(tempDir) + require.NoError(t, err) + }) + + for locale, fromLocale := range localesToCopy { + err = utils.CopyFile( + filepath.Join(i18nDir, fmt.Sprintf("%s.json", fromLocale)), + filepath.Join(tempDir, fmt.Sprintf("%s.json", locale)), + ) + require.NoError(t, err) + } + + return tempDir + } + + t.Run("unsupported locale ignored", func(t *testing.T) { + tempDir := setup(t, map[string]string{"en": "en", "fr": "fr", "zz": "en"}) + + err := initTranslationsWithDir(tempDir) + require.NoError(t, err) + + _, found := locales["zz"] + require.False(t, found, "should have ignored unsupported locale") + }) + + t.Run("malformed, unsupported locale ignored", func(t *testing.T) { + tempDir := setup(t, map[string]string{"en": "en", "fr": "fr", "zz": "en"}) + + err := os.WriteFile(filepath.Join(tempDir, "xx.json"), []byte{'{'}, os.ModePerm) + require.NoError(t, err) + + err = initTranslationsWithDir(tempDir) + require.NoError(t, err) + + _, found := locales["xx"] + require.False(t, found, "should have ignored malformed, unsupported locale") + }) + + t.Run("malformed, supported locale causes error", func(t *testing.T) { + tempDir := setup(t, map[string]string{"fr": "fr", "zz": "en"}) + + err := os.WriteFile(filepath.Join(tempDir, "en.json"), []byte{'{'}, os.ModePerm) + require.NoError(t, err) + + err = initTranslationsWithDir(tempDir) + require.Error(t, err, "should have failed to load malformed, supported locale") + }) + + t.Run("known locales loaded ", func(t *testing.T) { + tempDir := setup(t, map[string]string{"en": "en", "fr": "fr"}) + + err := initTranslationsWithDir(tempDir) + require.NoError(t, err) + + _, found := locales["en"] + require.True(t, found, "should have found en locale") + _, found = locales["fr"] + require.True(t, found, "should have found fr locale") + _, found = locales["es"] + require.False(t, found, "should not have found unloaded es locale") + }) +} + +func TestGetTranslationFuncForDir(t *testing.T) { + i18nDir, found := utils.FindDir("server/i18n") + require.True(t, found, "unable to find i18n dir") + + setup := func(t *testing.T, localesToCopy map[string]string) string { + tempDir, err := os.MkdirTemp(os.TempDir(), "TestGetTranslationFuncForDir") + require.NoError(t, err, "unable to create temporary directory") + + t.Cleanup(func() { + err = os.RemoveAll(tempDir) + require.NoError(t, err) + }) + + for locale, fromLocale := range localesToCopy { + err = utils.CopyFile( + filepath.Join(i18nDir, fmt.Sprintf("%s.json", fromLocale)), + filepath.Join(tempDir, fmt.Sprintf("%s.json", locale)), + ) + require.NoError(t, err) + } + + return tempDir + } + + t.Run("unknown locale falls back to english", func(t *testing.T) { + tempDir := setup(t, map[string]string{"en": "en", "fr": "fr", "zz": "en"}) + + translationFunc, err := GetTranslationFuncForDir(tempDir) + require.NoError(t, err) + require.NotNil(t, translationFunc) + + require.Equal(t, "December", translationFunc("unknown")("December")) + }) + + t.Run("unsupported locale falls back to english", func(t *testing.T) { + tempDir := setup(t, map[string]string{"en": "en", "fr": "fr", "zz": "en"}) + + translationFunc, err := GetTranslationFuncForDir(tempDir) + require.NoError(t, err) + require.NotNil(t, translationFunc) + + require.Equal(t, "December", translationFunc("zz")("December")) + }) + + t.Run("malformed, unsupported locale ignored and falls back to english", func(t *testing.T) { + tempDir := setup(t, map[string]string{"en": "en", "fr": "fr", "zz": "en"}) + + err := os.WriteFile(filepath.Join(tempDir, "xx.json"), []byte{'{'}, os.ModePerm) + require.NoError(t, err) + + translationFunc, err := GetTranslationFuncForDir(tempDir) + require.NoError(t, err) + require.NotNil(t, translationFunc) + + require.Equal(t, "December", translationFunc("xx")("December")) + }) + + t.Run("malformed, supported locale causes error", func(t *testing.T) { + tempDir := setup(t, map[string]string{"fr": "fr", "zz": "en"}) + + err := os.WriteFile(filepath.Join(tempDir, "en.json"), []byte{'{'}, os.ModePerm) + require.NoError(t, err) + + translationFunc, err := GetTranslationFuncForDir(tempDir) + require.Error(t, err) + require.Nil(t, translationFunc) + }) + + t.Run("known locale matches", func(t *testing.T) { + tempDir := setup(t, map[string]string{"en": "en", "fr": "fr"}) + + translationFunc, err := GetTranslationFuncForDir(tempDir) + require.NoError(t, err) + require.NotNil(t, translationFunc) + + require.Equal(t, "Décembre", translationFunc("fr")("December")) + require.Equal(t, "December", translationFunc("en")("December")) + }) +} diff --git a/server/public/shared/mlog/levels.go b/server/public/shared/mlog/levels.go index 1c88e816788..f640ea82a96 100644 --- a/server/public/shared/mlog/levels.go +++ b/server/public/shared/mlog/levels.go @@ -32,9 +32,6 @@ var ( LvlAuditPerms = Level{ID: 102, Name: "audit-permissions"} LvlAuditCLI = Level{ID: 103, Name: "audit-cli"} - // used by the TCP log target - LvlTCPLogTarget = Level{ID: 120, Name: "TcpLogTarget"} - // used by Remote Cluster Service LvlRemoteClusterServiceDebug = Level{ID: 130, Name: "RemoteClusterServiceDebug"} LvlRemoteClusterServiceError = Level{ID: 131, Name: "RemoteClusterServiceError"} @@ -46,10 +43,6 @@ var ( LvlSharedChannelServiceWarn = Level{ID: 202, Name: "SharedChannelServiceWarn"} LvlSharedChannelServiceMessagesInbound = Level{ID: 203, Name: "SharedChannelServiceMsgInbound"} LvlSharedChannelServiceMessagesOutbound = Level{ID: 204, Name: "SharedChannelServiceMsgOutbound"} - - // Focalboard - LvlFBTelemetry = Level{ID: 9000, Name: "telemetry"} - LvlFBMetrics = Level{ID: 9001, Name: "metrics"} ) // Combinations for LogM (log multi). diff --git a/server/public/shared/mlog/mlog.go b/server/public/shared/mlog/mlog.go index 060f3cd3029..5bfcac963ac 100644 --- a/server/public/shared/mlog/mlog.go +++ b/server/public/shared/mlog/mlog.go @@ -40,6 +40,7 @@ type LoggerIFace interface { LogM([]Level, string, ...Field) With(fields ...Field) *Logger Flush() error + Sugar(fields ...Field) Sugar StdLogger(level Level) *log.Logger } diff --git a/server/public/shared/mlog/tlog.go b/server/public/shared/mlog/tlog.go index ef8f6016a08..749e5591402 100644 --- a/server/public/shared/mlog/tlog.go +++ b/server/public/shared/mlog/tlog.go @@ -8,6 +8,7 @@ import ( "io" "os" "sync" + "testing" "github.com/mattermost/logr/v2" "github.com/mattermost/logr/v2/formatters" @@ -33,26 +34,66 @@ func AddWriterTarget(logger *Logger, w io.Writer, useJSON bool, levels ...Level) } // CreateConsoleTestLogger creates a logger for unit tests. Log records are output to `os.Stdout`. -// Logs can also be mirrored to the optional `io.Writer`. -func CreateConsoleTestLogger(useJSON bool, level Level) *Logger { - logger, _ := NewLogger() +// All log messages with level trace or lower are logged. +// The returned logger get Shutdown() when the tests completes. The caller should not shut it down. +func CreateConsoleTestLogger(tb testing.TB) *Logger { + tb.Helper() + + logger, err := NewLogger() + if err != nil { + tb.Fatalf("failed create logger %v", err) + } filter := logr.StdFilter{ - Lvl: level, + Lvl: LvlTrace, Stacktrace: LvlPanic, } - - var formatter logr.Formatter - if useJSON { - formatter = &formatters.JSON{EnableCaller: true} - } else { - formatter = &formatters.Plain{EnableCaller: true} - } + formatter := &formatters.Plain{EnableCaller: true} target := targets.NewWriterTarget(os.Stdout) if err := logger.log.Logr().AddTarget(target, "_testcon", filter, formatter, 1000); err != nil { - panic(err) + tb.Fatalf("failed to add target %v", err) } + + tb.Cleanup(func() { + err := logger.Shutdown() + if err != nil { + tb.Fatalf("failed to shut down test logger %v", err) + } + }) + + return logger +} + +// CreateTestLogger creates a logger for unit tests. Log records are output via `t.Log`. +// All log messages with level trace or lower are logged. +// The returned logger get Shutdown() when the tests completes. The caller should not shut it down. +func CreateTestLogger(t *testing.T) *Logger { + t.Helper() + + logger, err := NewLogger() + if err != nil { + t.Fatalf("failed create logger %v", err) + } + + filter := logr.StdFilter{ + Lvl: LvlTrace, + Stacktrace: LvlPanic, + } + formatter := &formatters.Plain{EnableCaller: true} + target := targets.NewTestingTarget(t) + + if err := logger.log.Logr().AddTarget(target, "test", filter, formatter, 1000); err != nil { + t.Fatalf("failed to add target %v", err) + } + + t.Cleanup(func() { + err := logger.Shutdown() + if err != nil { + t.Errorf("failed to shut down test logger %v", err) + } + }) + return logger } diff --git a/server/channels/app/request/context.go b/server/public/shared/request/context.go similarity index 81% rename from server/channels/app/request/context.go rename to server/public/shared/request/context.go index 29efaedeea0..d2d71a17681 100644 --- a/server/channels/app/request/context.go +++ b/server/public/shared/request/context.go @@ -5,6 +5,7 @@ package request import ( "context" + "testing" "github.com/mattermost/mattermost/server/public/model" "github.com/mattermost/mattermost/server/public/shared/i18n" @@ -16,21 +17,21 @@ type Context struct { session model.Session requestId string ipAddress string + xForwardedFor string path string userAgent string acceptLanguage string logger mlog.LoggerIFace - err *model.AppError context context.Context } -func NewContext(ctx context.Context, requestId, ipAddress, path, userAgent, acceptLanguage string, session model.Session, t i18n.TranslateFunc) *Context { +func NewContext(ctx context.Context, requestId, ipAddress, xForwardedFor, path, userAgent, acceptLanguage string, t i18n.TranslateFunc) *Context { return &Context{ t: t, - session: session, requestId: requestId, ipAddress: ipAddress, + xForwardedFor: xForwardedFor, path: path, userAgent: userAgent, acceptLanguage: acceptLanguage, @@ -46,6 +47,13 @@ func EmptyContext(logger mlog.LoggerIFace) *Context { } } +// TestContext creates an empty context with a new logger to use in testing where a test helper is +// not required. +func TestContext(t *testing.T) *Context { + logger := mlog.CreateConsoleTestLogger(t) + return EmptyContext(logger) +} + func (c *Context) T(translationID string, args ...any) string { return c.t(translationID, args...) } @@ -58,6 +66,9 @@ func (c *Context) RequestId() string { func (c *Context) IPAddress() string { return c.ipAddress } +func (c *Context) XForwardedFor() string { + return c.xForwardedFor +} func (c *Context) Path() string { return c.path } @@ -85,6 +96,9 @@ func (c *Context) SetRequestId(s string) { func (c *Context) SetIPAddress(s string) { c.ipAddress = s } +func (c *Context) SetXForwardedFor(s string) { + c.xForwardedFor = s +} func (c *Context) SetUserAgent(s string) { c.userAgent = s } @@ -110,19 +124,12 @@ func (c *Context) Logger() mlog.LoggerIFace { return c.logger } -func (c *Context) SetAppError(err *model.AppError) { - c.err = err -} - -func (c *Context) AppError() *model.AppError { - return c.err -} - type CTX interface { T(string, ...interface{}) string Session() *model.Session RequestId() string IPAddress() string + XForwardedFor() string Path() string UserAgent() string AcceptLanguage() string @@ -138,6 +145,4 @@ type CTX interface { GetT() i18n.TranslateFunc SetLogger(mlog.LoggerIFace) Logger() mlog.LoggerIFace - SetAppError(*model.AppError) - AppError() *model.AppError } diff --git a/server/public/utils/fileutils.go b/server/public/utils/fileutils.go new file mode 100644 index 00000000000..2418bc2f6ee --- /dev/null +++ b/server/public/utils/fileutils.go @@ -0,0 +1,118 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package utils + +import ( + "os" + "path/filepath" +) + +func CommonBaseSearchPaths() []string { + paths := []string{ + ".", + "..", + "../..", + "../../..", + "../../../..", + } + + // this enables the server to be used in tests from a different repository + if mmPath := os.Getenv("MM_SERVER_PATH"); mmPath != "" { + paths = append(paths, mmPath) + } + + return paths +} + +func findPath(path string, baseSearchPaths []string, workingDirFirst bool, filter func(os.FileInfo) bool) string { + if filepath.IsAbs(path) { + if _, err := os.Stat(path); err == nil { + return path + } + + return "" + } + + searchPaths := []string{} + if workingDirFirst { + searchPaths = append(searchPaths, baseSearchPaths...) + } + + // Attempt to search relative to the location of the running binary either before + // or after searching relative to the working directory, depending on `workingDirFirst`. + var binaryDir string + if exe, err := os.Executable(); err == nil { + if exe, err = filepath.EvalSymlinks(exe); err == nil { + if exe, err = filepath.Abs(exe); err == nil { + binaryDir = filepath.Dir(exe) + } + } + } + if binaryDir != "" { + for _, baseSearchPath := range baseSearchPaths { + searchPaths = append( + searchPaths, + filepath.Join(binaryDir, baseSearchPath), + ) + } + } + + if !workingDirFirst { + searchPaths = append(searchPaths, baseSearchPaths...) + } + + for _, parent := range searchPaths { + found, err := filepath.Abs(filepath.Join(parent, path)) + if err != nil { + continue + } else if fileInfo, err := os.Stat(found); err == nil { + if filter != nil { + if filter(fileInfo) { + return found + } + } else { + return found + } + } + } + + return "" +} + +func FindPath(path string, baseSearchPaths []string, filter func(os.FileInfo) bool) string { + return findPath(path, baseSearchPaths, true, filter) +} + +// FindFile looks for the given file in nearby ancestors relative to the current working +// directory as well as the directory of the executable. +func FindFile(path string) string { + return FindPath(path, CommonBaseSearchPaths(), func(fileInfo os.FileInfo) bool { + return !fileInfo.IsDir() + }) +} + +// fileutils.FindDir looks for the given directory in nearby ancestors relative to the current working +// directory as well as the directory of the executable, falling back to `./` if not found. +func FindDir(dir string) (string, bool) { + found := FindPath(dir, CommonBaseSearchPaths(), func(fileInfo os.FileInfo) bool { + return fileInfo.IsDir() + }) + if found == "" { + return "./", false + } + + return found, true +} + +// FindDirRelBinary looks for the given directory in nearby ancestors relative to the +// directory of the executable, then relative to the working directory, falling back to `./` if not found. +func FindDirRelBinary(dir string) (string, bool) { + found := findPath(dir, CommonBaseSearchPaths(), false, func(fileInfo os.FileInfo) bool { + return fileInfo.IsDir() + }) + if found == "" { + return "./", false + } + return found, true +} diff --git a/server/public/utils/fileutils_test.go b/server/public/utils/fileutils_test.go new file mode 100644 index 00000000000..d4781effc45 --- /dev/null +++ b/server/public/utils/fileutils_test.go @@ -0,0 +1,123 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package utils + +import ( + "fmt" + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestFindFile(t *testing.T) { + t.Run("files from various paths", func(t *testing.T) { + // Create the following directory structure: + // tmpDir1/ + // file1.json + // file2.xml + // other.txt + // tmpDir2/ + // other.txt/ [directory] + // tmpDir3/ + // tmpDir4/ + // tmpDir5/ + tmpDir1, err := os.MkdirTemp("", "") + require.NoError(t, err) + defer os.RemoveAll(tmpDir1) + + tmpDir2, err := os.MkdirTemp(tmpDir1, "") + require.NoError(t, err) + + err = os.Mkdir(filepath.Join(tmpDir2, "other.txt"), 0700) + require.NoError(t, err) + + tmpDir3, err := os.MkdirTemp(tmpDir2, "") + require.NoError(t, err) + + tmpDir4, err := os.MkdirTemp(tmpDir3, "") + require.NoError(t, err) + + tmpDir5, err := os.MkdirTemp(tmpDir4, "") + require.NoError(t, err) + + type testCase struct { + Description string + Cwd *string + FileName string + Expected string + } + + testCases := []testCase{} + + for _, fileName := range []string{"file1.json", "file2.xml", "other.txt"} { + filePath := filepath.Join(tmpDir1, fileName) + require.NoError(t, os.WriteFile(filePath, []byte("{}"), 0600)) + + // Relative paths end up getting symlinks fully resolved, so use this below as necessary. + filePathResolved, err := filepath.EvalSymlinks(filePath) + require.NoError(t, err) + + testCases = append(testCases, []testCase{ + { + fmt.Sprintf("absolute path to %s", fileName), + nil, + filePath, + filePath, + }, + { + fmt.Sprintf("absolute path to %s from containing directory", fileName), + &tmpDir1, + filePath, + filePath, + }, + { + fmt.Sprintf("relative path to %s from containing directory", fileName), + &tmpDir1, + fileName, + filePathResolved, + }, + { + fmt.Sprintf("%s: subdirectory of containing directory", fileName), + &tmpDir2, + fileName, + filePathResolved, + }, + { + fmt.Sprintf("%s: twice-nested subdirectory of containing directory", fileName), + &tmpDir3, + fileName, + filePathResolved, + }, + { + fmt.Sprintf("%s: thrice-nested subdirectory of containing directory", fileName), + &tmpDir4, + fileName, + filePathResolved, + }, + { + fmt.Sprintf("%s: quadruple-nested subdirectory of containing directory", fileName), + &tmpDir5, + fileName, + filePathResolved, + }, + }...) + } + + for _, testCase := range testCases { + t.Run(testCase.Description, func(t *testing.T) { + if testCase.Cwd != nil { + prevDir, err := os.Getwd() + require.NoError(t, err) + defer os.Chdir(prevDir) + os.Chdir(*testCase.Cwd) + } + + assert.Equal(t, testCase.Expected, FindFile(testCase.FileName)) + }) + } + }) +} diff --git a/server/public/utils/utils.go b/server/public/utils/utils.go new file mode 100644 index 00000000000..fcadce230e2 --- /dev/null +++ b/server/public/utils/utils.go @@ -0,0 +1,14 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package utils + +// Contains returns true if the slice contains the item. +func Contains[T comparable](slice []T, item T) bool { + for _, s := range slice { + if s == item { + return true + } + } + return false +} diff --git a/server/public/utils/utils_test.go b/server/public/utils/utils_test.go new file mode 100644 index 00000000000..856f76886ca --- /dev/null +++ b/server/public/utils/utils_test.go @@ -0,0 +1,388 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package utils_test + +import ( + "testing" + + "github.com/mattermost/mattermost/server/public/utils" +) + +func TestContains(t *testing.T) { + testCasesStr := []struct { + name string + slice []string + item string + expected bool + }{ + { + name: "empty slice", + slice: []string{}, + item: "foo", + expected: false, + }, + { + name: "slice with item", + slice: []string{"foo"}, + item: "foo", + expected: true, + }, + { + name: "slice without item", + slice: []string{"bar"}, + item: "foo", + expected: false, + }, + { + name: "slice with multiple items", + slice: []string{"foo", "bar"}, + item: "foo", + expected: true, + }, + { + name: "slice with multiple items without item", + slice: []string{"foo", "bar"}, + item: "baz", + expected: false, + }, + } + + for _, tc := range testCasesStr { + t.Run(tc.name, func(t *testing.T) { + actual := utils.Contains(tc.slice, tc.item) + if actual != tc.expected { + t.Errorf("Expected Contains(%v, %v) to be %v, but got %v", tc.slice, tc.item, tc.expected, actual) + } + }) + } + + testCasesInt := []struct { + name string + slice []int + item int + expected bool + }{ + { + name: "empty slice", + slice: []int{}, + item: 1, + expected: false, + }, + { + name: "slice with item", + slice: []int{1}, + item: 1, + expected: true, + }, + { + name: "slice without item", + slice: []int{2}, + item: 1, + expected: false, + }, + { + name: "slice with multiple items", + slice: []int{1, 2}, + item: 1, + expected: true, + }, + { + name: "slice with multiple items without item", + slice: []int{1, 2}, + item: 3, + expected: false, + }, + } + + for _, tc := range testCasesInt { + t.Run(tc.name, func(t *testing.T) { + actual := utils.Contains(tc.slice, tc.item) + if actual != tc.expected { + t.Errorf("Expected Contains(%v, %v) to be %v, but got %v", tc.slice, tc.item, tc.expected, actual) + } + }) + } + + testCasesFloat := []struct { + name string + slice []float64 + item float64 + expected bool + }{ + { + name: "empty slice", + slice: []float64{}, + item: 1.0, + expected: false, + }, + { + name: "slice with item", + slice: []float64{1.0}, + item: 1.0, + expected: true, + }, + { + name: "slice without item", + slice: []float64{2.0}, + item: 1.0, + expected: false, + }, + { + name: "slice with multiple items", + slice: []float64{1.0, 2.0}, + item: 1.0, + expected: true, + }, + { + name: "slice with multiple items without item", + slice: []float64{1.0, 2.0}, + item: 3.0, + expected: false, + }, + } + + for _, tc := range testCasesFloat { + t.Run(tc.name, func(t *testing.T) { + actual := utils.Contains(tc.slice, tc.item) + if actual != tc.expected { + t.Errorf("Expected Contains(%v, %v) to be %v, but got %v", tc.slice, tc.item, tc.expected, actual) + } + }) + } + + testCasesBool := []struct { + name string + slice []bool + item bool + expected bool + }{ + { + name: "empty slice", + slice: []bool{}, + item: true, + expected: false, + }, + { + name: "slice with item", + slice: []bool{true}, + item: true, + expected: true, + }, + { + name: "slice without item", + slice: []bool{false}, + item: true, + expected: false, + }, + { + name: "slice with multiple items", + slice: []bool{true, false}, + item: true, + expected: true, + }, + { + name: "slice with multiple items without item", + slice: []bool{true, false}, + item: false, + expected: true, + }, + } + + for _, tc := range testCasesBool { + t.Run(tc.name, func(t *testing.T) { + actual := utils.Contains(tc.slice, tc.item) + if actual != tc.expected { + t.Errorf("Expected Contains(%v, %v) to be %v, but got %v", tc.slice, tc.item, tc.expected, actual) + } + }) + } + + testCasesByte := []struct { + name string + slice []byte + item byte + expected bool + }{ + { + name: "empty slice", + slice: []byte{}, + item: 1, + expected: false, + }, + { + name: "slice with item", + slice: []byte{1}, + item: 1, + expected: true, + }, + { + name: "slice without item", + slice: []byte{2}, + item: 1, + expected: false, + }, + { + name: "slice with multiple items", + slice: []byte{1, 2}, + item: 1, + expected: true, + }, + { + name: "slice with multiple items without item", + slice: []byte{1, 2}, + item: 3, + expected: false, + }, + } + + for _, tc := range testCasesByte { + t.Run(tc.name, func(t *testing.T) { + actual := utils.Contains(tc.slice, tc.item) + if actual != tc.expected { + t.Errorf("Expected Contains(%v, %v) to be %v, but got %v", tc.slice, tc.item, tc.expected, actual) + } + }) + } + + testCasesRune := []struct { + name string + slice []rune + item rune + expected bool + }{ + { + name: "empty slice", + slice: []rune{}, + item: 1, + expected: false, + }, + { + name: "slice with item", + slice: []rune{1}, + item: 1, + expected: true, + }, + { + name: "slice without item", + slice: []rune{2}, + item: 1, + expected: false, + }, + { + name: "slice with multiple items", + slice: []rune{1, 2}, + item: 1, + expected: true, + }, + { + name: "slice with multiple items without item", + slice: []rune{1, 2}, + item: 3, + expected: false, + }, + } + + for _, tc := range testCasesRune { + t.Run(tc.name, func(t *testing.T) { + actual := utils.Contains(tc.slice, tc.item) + if actual != tc.expected { + t.Errorf("Expected Contains(%v, %v) to be %v, but got %v", tc.slice, tc.item, tc.expected, actual) + } + }) + } + + testCasesComplex := []struct { + name string + slice []complex128 + item complex128 + expected bool + }{ + { + name: "empty slice", + slice: []complex128{}, + item: 1, + expected: false, + }, + { + name: "slice with item", + slice: []complex128{1}, + item: 1, + expected: true, + }, + { + name: "slice without item", + slice: []complex128{2}, + item: 1, + expected: false, + }, + { + name: "slice with multiple items", + slice: []complex128{1, 2}, + item: 1, + expected: true, + }, + { + name: "slice with multiple items without item", + slice: []complex128{1, 2}, + item: 3, + expected: false, + }, + } + + for _, tc := range testCasesComplex { + t.Run(tc.name, func(t *testing.T) { + actual := utils.Contains(tc.slice, tc.item) + if actual != tc.expected { + t.Errorf("Expected Contains(%v, %v) to be %v, but got %v", tc.slice, tc.item, tc.expected, actual) + } + }) + } + + testCasesUint := []struct { + name string + slice []uint + item uint + expected bool + }{ + { + name: "empty slice", + slice: []uint{}, + item: 1, + expected: false, + }, + { + name: "slice with item", + slice: []uint{1}, + item: 1, + expected: true, + }, + { + name: "slice without item", + slice: []uint{2}, + item: 1, + expected: false, + }, + { + name: "slice with multiple items", + slice: []uint{1, 2}, + item: 1, + expected: true, + }, + { + name: "slice with multiple items without item", + slice: []uint{1, 2}, + item: 3, + expected: false, + }, + } + + for _, tc := range testCasesUint { + t.Run(tc.name, func(t *testing.T) { + actual := utils.Contains(tc.slice, tc.item) + if actual != tc.expected { + t.Errorf("Expected Contains(%v, %v) to be %v, but got %v", tc.slice, tc.item, tc.expected, actual) + } + }) + } +} diff --git a/server/scripts/mirror-docker-images.json b/server/scripts/mirror-docker-images.json index d3758a1fba8..5823ea12a3f 100644 --- a/server/scripts/mirror-docker-images.json +++ b/server/scripts/mirror-docker-images.json @@ -28,13 +28,14 @@ "8.0.1": "grafana/grafana:8.0.1@sha256:1c3e2fc7896adf9e33be5d062c08066087cb556f63b0a95f8aefe92bd37a6f38" }, "cypress-browsers-public": { - "node16.14.2-slim-chrome100-ff99-edge": "cypress/browsers:node16.14.2-slim-chrome100-ff99-edge@sha256:f8459ee677ce356eff64698095fbdf48eb9ab018fc5eb0d30c07ba23884edace" - }, - "golang": { - "1.19.5": "golang:1.19.5@sha256:bb9811fad43a7d6fd2173248d8331b2dcf5ac9af20976b1937ecd214c5b8c383" + "node16.14.2-slim-chrome100-ff99-edge": "cypress/browsers:node16.14.2-slim-chrome100-ff99-edge@sha256:f8459ee677ce356eff64698095fbdf48eb9ab018fc5eb0d30c07ba23884edace", + "node-18.16.1-chrome-114.0.5735.133-1-ff-114.0.2-edge-114.0.1823.51-1": "cypress/browsers:node-18.16.1-chrome-114.0.5735.133-1-ff-114.0.2-edge-114.0.1823.51-1@sha256:f798d6cc25d724210be3660e95f63ae75d052ef392e3fc5ae728d02da7e52df8" }, "node": { "16.10.0": "node:16.10.0", - "16.17.0": "node:16.17.0" + "16.17.0": "node:16.17.0", + "18": "node:18@sha256:11e9c297fc51f6f65f7d0c7c8a8581e5721f2f16de43ceff1a199fd3ef609f95", + "18.17": "node:18.17@sha256:11e9c297fc51f6f65f7d0c7c8a8581e5721f2f16de43ceff1a199fd3ef609f95", + "18.17.1": "node:18.17.1@sha256:11e9c297fc51f6f65f7d0c7c8a8581e5721f2f16de43ceff1a199fd3ef609f95" } } diff --git a/server/scripts/wait-for-system-start.sh b/server/scripts/wait-for-system-start.sh index 109ce4c7208..612be62f60e 100755 --- a/server/scripts/wait-for-system-start.sh +++ b/server/scripts/wait-for-system-start.sh @@ -1,11 +1,11 @@ #!/bin/bash total=0 -max_wait_seconds=60 +MAX_WAIT_SECONDS="${MAX_WAIT_SECONDS:=60}" -echo "waiting $max_wait_seconds seconds for the server to start" +echo "waiting $MAX_WAIT_SECONDS seconds for the server to start" -while [[ "$total" -le "$max_wait_seconds" ]]; do +while [[ "$total" -le "$MAX_WAIT_SECONDS" ]]; do if bin/mmctl system status --local 2> /dev/null; then exit 0 else @@ -15,6 +15,6 @@ while [[ "$total" -le "$max_wait_seconds" ]]; do fi done -printf "\nserver didn't start in $max_wait_seconds seconds\n" +printf "\nserver didn't start in $MAX_WAIT_SECONDS seconds\n" exit 1 \ No newline at end of file diff --git a/server/tests/README.md b/server/tests/README.md index d259132da5c..60481b41438 100644 --- a/server/tests/README.md +++ b/server/tests/README.md @@ -19,7 +19,7 @@ It is possible to manually test specific sections of any test, instead of using ## Test plugins -There are two test plugins: `testplugin.tar.gz` and `testplugin2.tar.gz`. These are use in some integration tests in the `api4` package. Any changes to the plugin bundles require updating the corresponding signatures. +There are three test plugins: `testplugin.tar.gz`, `testplugin-v0.0.2.tar.gz`, and `testplugin2.tar.gz`. These are use in some integration tests in the `api4` package. Any changes to the plugin bundles require updating the corresponding signatures. First, import the public and private development key: ```sh @@ -33,6 +33,8 @@ Then update the signatures: ```sh gpg -u F3FACE45E0DE642C8BD6A8E64C7C6562C192CC1F --verbose --personal-digest-preferences SHA256 --detach-sign testplugin.tar.gz gpg -u F3FACE45E0DE642C8BD6A8E64C7C6562C192CC1F --verbose --personal-digest-preferences SHA256 --detach-sign --armor testplugin.tar.gz +gpg -u F3FACE45E0DE642C8BD6A8E64C7C6562C192CC1F --verbose --personal-digest-preferences SHA256 --detach-sign testplugin-v0.0.2.tar.gz +gpg -u F3FACE45E0DE642C8BD6A8E64C7C6562C192CC1F --verbose --personal-digest-preferences SHA256 --detach-sign --armor testplugin-v0.0.2.tar.gz gpg -u F3FACE45E0DE642C8BD6A8E64C7C6562C192CC1F --verbose --personal-digest-preferences SHA256 --detach-sign testplugin2.tar.gz gpg -u F3FACE45E0DE642C8BD6A8E64C7C6562C192CC1F --verbose --personal-digest-preferences SHA256 --detach-sign --armor testplugin2.tar.gz ``` diff --git a/server/tests/template.load b/server/tests/template.load new file mode 100644 index 00000000000..e6400e66517 --- /dev/null +++ b/server/tests/template.load @@ -0,0 +1,42 @@ +LOAD DATABASE + FROM mysql://{{ .mysql_user }}:{{ .mysql_password }}@mysql:3306/{{ .source_schema }} + INTO pgsql://{{ .pg_user }}:{{ .pg_password }}@postgres:5432/{{ .target_schema }} + +WITH data only, + workers = 8, concurrency = 1, + multiple readers per thread, rows per range = 50000, + create no tables, create no indexes, + preserve index names + +SET PostgreSQL PARAMETERS + maintenance_work_mem to '128MB', + work_mem to '12MB' + +SET MySQL PARAMETERS + net_read_timeout = '120', + net_write_timeout = '120' + +CAST column Channels.Type to "channel_type" drop typemod, + column Teams.Type to "team_type" drop typemod, + column UploadSessions.Type to "upload_session_type" drop typemod, + column Drafts.Priority to text, + type int when (= precision 11) to integer drop typemod, + type bigint when (= precision 20) to bigint drop typemod, + type text to varchar drop typemod, + type tinyint when (<= precision 4) to boolean using tinyint-to-boolean, + type json to jsonb drop typemod + +EXCLUDING TABLE NAMES MATCHING ~, ~ + +BEFORE LOAD DO + $$ ALTER SCHEMA public RENAME TO {{ .source_schema }}; $$, + $$ DROP INDEX IF EXISTS idx_posts_message_txt; $$, + $$ DROP INDEX IF EXISTS idx_fileinfo_content_txt; $$ + +AFTER LOAD DO + $$ UPDATE {{ .source_schema }}.db_migrations set name='add_createat_to_teamembers' where version=92; $$, + $$ CREATE INDEX IF NOT EXISTS idx_posts_message_txt ON {{ .source_schema }}.posts USING gin(to_tsvector('english', message)); $$, + $$ CREATE INDEX IF NOT EXISTS idx_fileinfo_content_txt ON {{ .source_schema }}.fileinfo USING gin(to_tsvector('english', content)); $$, + $$ ALTER SCHEMA {{ .source_schema }} RENAME TO public; $$, + $$ SELECT pg_catalog.set_config('search_path', '"$user", public', false); $$, + $$ ALTER USER {{ .pg_user }} SET SEARCH_PATH TO 'public'; $$; diff --git a/server/tests/testplugin-v0.0.2.tar.gz b/server/tests/testplugin-v0.0.2.tar.gz new file mode 100644 index 00000000000..5629ced271a Binary files /dev/null and b/server/tests/testplugin-v0.0.2.tar.gz differ diff --git a/server/tests/testplugin-v0.0.2.tar.gz.asc b/server/tests/testplugin-v0.0.2.tar.gz.asc new file mode 100644 index 00000000000..9f164071030 --- /dev/null +++ b/server/tests/testplugin-v0.0.2.tar.gz.asc @@ -0,0 +1,14 @@ +-----BEGIN PGP SIGNATURE----- + +iQGzBAABCAAdFiEE8/rOReDeZCyL1qjmTHxlYsGSzB8FAmTVE0YACgkQTHxlYsGS +zB/eiAv/V5PWAJesWktjFIWWviuiwnL3orGrlPVQ4c5d3pHslyTr7uyzAvyOhH9w +Eg7BZC3vObV61d2XAseU0+hCUjZ7JHjRwyhTxdVj0fsuaDgl1mBbHTtZXPCKL59P +RfXf8urkzzlSVYnMgeH158KTBtVyDaNENrhytIkNaxVy3k50tga6Z4GgN+wTfyQw +FdLIT2piYb9UZZQT8mtw8l/Z/kkV0E038GK3PI1lLlhPHrvIo9kYG/11R061kXgg +GxiuNEHdSftI/ZHF8f/QoPQosT5nZavhLsGE7eD+YBhDiL61cArcenGPKEsQSIIr +K+9su+u/7XMqFX4GgsMs7uvxWz4o1ihK9l4wQ8aLDcYolUHqzplwOcQHojGutmiF +XuqL17s7gxYTxRMHDmaePuI9mnti7cVFrSXLJwsy4qt7nAKcT2IP/qR8VBLE3R3f +Gm15AaivKON24Oj7URz4hQfQXbNj0Ve4Pmiq85hT0VXZZAVCpuxww0J7Q0EXvaBg +9dzylz8J +=L0I2 +-----END PGP SIGNATURE----- diff --git a/server/tests/testplugin-v0.0.2.tar.gz.sig b/server/tests/testplugin-v0.0.2.tar.gz.sig new file mode 100644 index 00000000000..47880e6a960 Binary files /dev/null and b/server/tests/testplugin-v0.0.2.tar.gz.sig differ diff --git a/server/tests/testplugin.tar.gz.asc b/server/tests/testplugin.tar.gz.asc index 7a7885f779b..262af885b61 100644 --- a/server/tests/testplugin.tar.gz.asc +++ b/server/tests/testplugin.tar.gz.asc @@ -1,14 +1,14 @@ -----BEGIN PGP SIGNATURE----- -iQGzBAABCAAdFiEE8/rOReDeZCyL1qjmTHxlYsGSzB8FAmOaRXoACgkQTHxlYsGS -zB8H1Av+MuNxBuQFxNvORGcudExCAAgQZb3ykYNVxPT1CzwVdd16B+VvRyt3+PKz -nSsyYyrdvd2xpdaEXFHBA8RxS5ZWCz/hkrdxUhBUWV8O5OUMOHDYvetWc6/9GeuR -3dd4VElpLsEs6hIpwnejR1EouNr5OhxstpnB2AOz5N7LWlG5lTKhaHs1zN1uLc4f -GmdJZ+5+PYm1UUipFj4kolkI+44Ytl6mj+tTyC4VJAj0mwnXQtp/JdFcDmmeRrTI -AwmanJKQlK3yw331FYSd/CXuqCGOh157X7Z5P2Mtr3ZOaNj7qLY0mjweqxjj4fnN -YTUu22KhRLGzggEbTg+5huYhtvqa1b87EcH6ukxWoBYQpFK+TyyhuX3ZeT5x6lFi -8SP9o/9KcQxB5oD6X5FGMR4v5VDosNnNuqW8G7g4fkcjQKY65tnX75G5Ih156BAP -dfZ6+nKCQvXgv1XRymF3UrJeddXtOMQzh4aSvYwwy46qNMPYMeDq6PoMXGVNeylP -bTrjLEtE -=OvPw +iQGzBAABCAAdFiEE8/rOReDeZCyL1qjmTHxlYsGSzB8FAmTVE0YACgkQTHxlYsGS +zB/3YgwAlpt6B7w//bGGcbi1pGZU90Pz7qwDckvKQHAf1fh6BEKDb7lmFfw2okfI +0PIZ5tYiVrPI2FRoNMWUrJhqBvMJ22fi24ZXT4KHPAbm/tC9QmuxWA/sXHlx8EEM +7LhIBlBpaOd7F5Qu1PyLgfRWL/k/US4xm54yhpyDf61TiwiHuG/dBaTHTHoDhlKL +rTrkXngQ1DbKjOSj9XxVuMS7BMV5QMT0fex3BEA2hSPHdWqt209mgYalxzV6vGp3 +yjO1uGTfZi0P7S+F+QCZX4DYq5NMwlNEAt0OQbyE4rhI637nkh52mO9GUjezyPyw +eFeoUakwhbPMf6wdiBj378Vng+ja2HoN18ZGxmA4/2dF5w6ShlkPJuyjfEsqp8kr +A6wnNp+iSf2t2zam8AbN1cfiSYDJgJKcfecdO2gU/Bu6khqr3IPrjuFVR4ObSaV9 +kVoQo9rcu0gDybDjDHBpwgJwEIVwtEXwVyFe5fC7lK5Pc8oPz4pz6GtCo9OdnPr9 +dEFcQAtF +=w4gX -----END PGP SIGNATURE----- diff --git a/server/tests/testplugin.tar.gz.sig b/server/tests/testplugin.tar.gz.sig index 280f77dc126..eba987cdeb6 100644 Binary files a/server/tests/testplugin.tar.gz.sig and b/server/tests/testplugin.tar.gz.sig differ diff --git a/server/tests/testplugin2.tar.gz.asc b/server/tests/testplugin2.tar.gz.asc index e460b3fd077..0f62d298016 100644 --- a/server/tests/testplugin2.tar.gz.asc +++ b/server/tests/testplugin2.tar.gz.asc @@ -1,14 +1,14 @@ -----BEGIN PGP SIGNATURE----- -iQGzBAABCAAdFiEE8/rOReDeZCyL1qjmTHxlYsGSzB8FAl4soo8ACgkQTHxlYsGS -zB+7RwwAmwLH64J1G9LUQRD1rJFlvlIGgEE6leaJO4bKkOACD4QQs70q+rLzNazA -maVKjOddKq5fslIsQxHHlIeIMfuTioQ12ZOU6pVY1o1gw5RPolCopLdgac1lA0Fg -dDMCtB62iRJqcsnUWFJwhf9fBCwNVGjG6+fPnLyHOXNmDGWDnxxPLNevSEya/Yzg -68C1ervBJ4AV6x6Wyc4ejaAliHOBMzv1Z8NclWisKYllaigUUtWauctiM00Ga7kM -ADOMVBljI8QauV7E39mxr89Z9ULo8moI2dIPwyrC2vWaW0RnK1XXO2+pDugrlSN7 -ZLGFMdcD6kjnwaZXfSNXeXh2PeERjhRk8Zw5mvrg51VgUaqv2pZjP4g2hA5D1D0U -BGgPLVrlkqEV9my8MNLMhBmj1WfBzDNMEcUu4UbeVpnkj3Px0cnWAAFQgSAMniON -c8ng0VEuEHo2z3kiQcBT//mHgFuzaX47OYgPsBPZzMpIAlQ+giryHT8byXcOLcfc -KuwzQRZj -=Z3mM +iQGzBAABCAAdFiEE8/rOReDeZCyL1qjmTHxlYsGSzB8FAmTVE0cACgkQTHxlYsGS +zB+ikwwAyBpTvmyUT+WEnK8H/Ppdk8bGoPH8CdygOwR3Ss57IEd7lJWSVT2Wqnde +3aINnwa5Q6vHPEOiNFA+FiHGOonLlB7a4tqb2/Je/cWYyGSz8Fq+UNp9qyo5s/0s +xoDQrraNJIXQ9e+G0IiyL/aN8A+DnJVp9wEFkHDnp0eqDyRwULlK/HWwjnnIOk0K +vSae7T610SCjYgLT+S+QZ8/hefzhEYq6S54qN+LL3TdGcveFQNUwgVKcHipCo1s1 +WffRcC24Rt9LiQd00CAdQCKGoTl3KjxXsU70oeDCXHGvzod0iH/OIww437ewz0EU +/f+HVHHJAopbp947KUv5yvlGfqMXjgtDPKDyu4WtiK/BRkX7Wd9+PMLCLjn5URLx +9s+c0+5/uBdIxPwBzvdlcogoR0pmGcR1MZwias83mLYuaABsZsDVdNxsRTSVtEzj +Ko+2rvw3Yp3xdxPa/lXYFpf74e/oqqcY3qM0BHc+cnWBnTiTc3RfhnyJI98U3RzV +G78oFS/3 +=Y1mv -----END PGP SIGNATURE----- diff --git a/server/tests/testplugin2.tar.gz.sig b/server/tests/testplugin2.tar.gz.sig index 90f6d24be4d..52996bf0ce5 100644 Binary files a/server/tests/testplugin2.tar.gz.sig and b/server/tests/testplugin2.tar.gz.sig differ diff --git a/tools/mmgotool/commands/i18n.go b/tools/mmgotool/commands/i18n.go index 5e5f5474a8e..3aaf9dfea2d 100644 --- a/tools/mmgotool/commands/i18n.go +++ b/tools/mmgotool/commands/i18n.go @@ -681,6 +681,11 @@ func clean(translationDir string, file string, dryRun bool, check bool) (*string return nil, err } + if strings.TrimSpace(string(oldJSON)) == "{}" { + noIssue := "" + return &noIssue, err + } + var oldList []Item if err = json.Unmarshal(oldJSON, &oldList); err != nil { return nil, err diff --git a/tools/mmgotool/go.mod b/tools/mmgotool/go.mod index 6fca8c5b762..91221c21d0b 100644 --- a/tools/mmgotool/go.mod +++ b/tools/mmgotool/go.mod @@ -1,6 +1,6 @@ module github.com/mattermost/mattermost/tools/mmgotool -go 1.19 +go 1.20 require github.com/spf13/cobra v1.7.0 diff --git a/webapp/channels/.eslintrc.json b/webapp/channels/.eslintrc.json index 2451b34711d..b4b6d8b0450 100644 --- a/webapp/channels/.eslintrc.json +++ b/webapp/channels/.eslintrc.json @@ -1,11 +1,11 @@ { "root": true, "extends": [ - "plugin:mattermost/react", + "plugin:@mattermost/react", "plugin:react-hooks/recommended" ], "plugins": [ - "mattermost", + "@mattermost", "import", "no-only-tests", "@typescript-eslint", @@ -23,6 +23,7 @@ } }, "rules": { + "@mattermost/use-external-link": 2, "header/header": [ 2, "line", @@ -30,26 +31,56 @@ 2 ], "no-duplicate-imports": 0, - "mattermost/use-external-link": 2, "import/no-duplicates": 2, "max-nested-callbacks": ["error", 10], "no-unused-expressions": 0, "eol-last": ["error", "always"], "import/no-unresolved": 2, "import/order": [ - 0, + 2, { - "newlines-between": "always-and-inside-groups", + "newlines-between": "always", "groups": [ "builtin", "external", - [ - "internal", - "parent" - ], + "internal", "sibling", + "parent", "index" - ] + ], + "pathGroups": [ + { + "pattern": "@mattermost/**", + "group": "external", + "position": "after" + }, + { + "pattern": "mattermost-redux/**", + "group": "external", + "position": "after" + }, + { + "pattern": "@(selectors|actions|stores|store|reducers){,/**}", + "group": "external", + "position": "after" + }, + { + "pattern": "components/**", + "group": "external", + "position": "after" + }, + { + "pattern": "types{,/**}", + "group": "internal", + "position": "after" + } + ], + "alphabetize": { + "order": "asc", + "caseInsensitive": true + }, + "distinctGroup": true, + "pathGroupsExcludedImportTypes": ["builtin"] } ], "no-undefined": 0, @@ -95,7 +126,8 @@ } ], "max-lines": ["warn", {"max": 800, "skipBlankLines": true, "skipComments": true}], - "formatjs/no-multiple-whitespaces": 2 + "formatjs/no-multiple-whitespaces": 2, + "@typescript-eslint/consistent-type-imports": ["error", {"disallowTypeAnnotations": false}] }, "overrides": [ { diff --git a/webapp/channels/.gitlab-ci.yml b/webapp/channels/.gitlab-ci.yml deleted file mode 100644 index 631c151e282..00000000000 --- a/webapp/channels/.gitlab-ci.yml +++ /dev/null @@ -1,69 +0,0 @@ -stages: - - test - - build - - s3 - - trigger - -variables: - IMAGE_BUILD_WEBAPP: $CI_REGISTRY/mattermost/ci/devops-images/mattermost-build-webapp:20220802_node-16.10.0@sha256:3272aa759f10c2ef1719ed08cc82ddb07224bec5be86f09800c72f5e2a623c3d - BUILD: "yes" - TEST: "yes" - -include: - - project: mattermost/ci/mattermost-webapp - ref: master - file: private.yml - -empty: - stage: test - script: - - echo "empty" - -test: #todo check on how to create junit.xml, there currently is none - stage: test - image: ${IMAGE_BUILD_WEBAPP} - cache: - key: - files: - - package.json - prefix: ${CI_COMMIT_REF_SLUG} - paths: - - .npm/ - before_script: - - 'which ssh-agent || ( apt-get update -y && apt-get install openssh-client git -y )' - - eval $(ssh-agent -s) - - echo "$SSH_PRIVATE_KEY_GITHUB" | tr -d '\r' | ssh-add - - - mkdir -p ~/.ssh - - chmod 700 ~/.ssh - - ssh-keyscan github.com >> ~/.ssh/known_hosts - - chmod 644 ~/.ssh/known_hosts - script: - - npm ci --cache .npm --prefer-offline - - # For Playwright type check - - (cd e2e/playwright && npm ci --cache .npm --prefer-offline) - - # i18n - - cp i18n/en.json /tmp/en.json - - mkdir -p /tmp/fake-mobile-dir/assets/base/i18n/ - - echo '{}' > /tmp/fake-mobile-dir/assets/base/i18n/en.json - - - npm run mmjstool -- i18n extract-webapp --webapp-dir . --mobile-dir /tmp/fake-mobile-dir - - diff /tmp/en.json i18n/en.json - # Address weblate behavior which does not remove whole translation item when translation string is set to empty - - npm run mmjstool -- i18n clean-empty --webapp-dir . --mobile-dir /tmp/fake-mobile-dir --check - - npm run mmjstool -- i18n check-empty-src --webapp-dir . --mobile-dir /tmp/fake-mobile-dir - - - rm -rf tmp - - # typescript - - npm run check-types - - # lint - - npm run check - - # unit tests - - npm run test-ci - timeout: 3 hours - rules: - - if: '$CI_COMMIT_REF_NAME == "master" && $TEST == "yes"' diff --git a/webapp/channels/NOTICE.txt b/webapp/channels/NOTICE.txt index 0b44c073163..42aa15e5530 100644 --- a/webapp/channels/NOTICE.txt +++ b/webapp/channels/NOTICE.txt @@ -79,6 +79,32 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +--- + +## @giphy/js-fetch-api + +This product contains '@giphy/js-fetch-api' by Giphy. + +Javascript API to fetch gifs and stickers from the GIPHY API. + +* HOMEPAGE: + * https://github.com/Giphy/giphy-js/tree/master/packages/fetch-api + +* LICENSE: MIT + +--- + +## @giphy/react-components + +This product contains '@giphy/react-components' by Giphy. + +The GIPHY React SDK lets you and your users easily access the world’s largest GIF library anywhere on your website. + +* HOMEPAGE: + * https://github.com/Giphy/giphy-js/tree/master/packages/react-components + +* LICENSE: MIT + --- ## @guyplusplus/turndown-plugin-gfm @@ -1859,42 +1885,6 @@ trademarks does not indicate endorsement of the trademark holder by Font Awesome, nor vice versa. **Please do not use brand logos for any purpose except to represent the company, product, or service to which they refer.** ---- - -## gfycat-sdk - -This product contains 'gfycat-sdk' by gfycat. - -Javascript API wrapper for Gfycat.com API that supports callbacks and promises. - -* HOMEPAGE: - * https://github.com/gfycat/gfycat-sdk - -* LICENSE: MIT - -MIT License - -Copyright (c) 2016 Josh Kang - -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. - - --- ## highlight.js @@ -1933,7 +1923,6 @@ 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. - --- ## history diff --git a/webapp/channels/build/contrib/gitlab/.gitlab-ci.yml b/webapp/channels/build/contrib/gitlab/.gitlab-ci.yml deleted file mode 100644 index 9d9bb1c2465..00000000000 --- a/webapp/channels/build/contrib/gitlab/.gitlab-ci.yml +++ /dev/null @@ -1,16 +0,0 @@ -lint: - allow_failure: true - image: node:buster - before_script: - - npm ci --ignore-scripts - - (cd e2e/playwright && npm ci --ignore-scripts) - script: - - npm run check-types - - npm run check - -test: - image: node:buster - before_script: - - npm ci --ignore-scripts - script: - - npm run test diff --git a/webapp/channels/build/contrib/gitlab/README.md b/webapp/channels/build/contrib/gitlab/README.md deleted file mode 100644 index f94e7b5fe97..00000000000 --- a/webapp/channels/build/contrib/gitlab/README.md +++ /dev/null @@ -1,7 +0,0 @@ -# GitLab CI/CD configuration for Mattermost - -The [.gitlab-ci.yml](./.gitlab-ci.yml) file in this directory provides a [GitLab CI/CD configuration file](https://docs.gitlab.com/ee/ci/yaml/gitlab_ci_yaml.html) that can be used to run linting and unit testing for the Mattermost application. - -## Usage - -To configure your GitLab instance to use this configuration file (and without having to move it to the base directory in this git repository), you can configure the [`CI_CONFIG_PATH` predefined variable](https://docs.gitlab.com/ee/ci/variables/predefined_variables.html) for the repository in GitLab with the configuration file path (`build/contrib/gitlab/.gitlab-ci.yml`). diff --git a/webapp/channels/package.json b/webapp/channels/package.json index 65c7b4a6a3a..90aff87d39d 100644 --- a/webapp/channels/package.json +++ b/webapp/channels/package.json @@ -3,15 +3,17 @@ "browser": { "./client/web_client.jsx": "./client/browser_web_client.jsx" }, - "version": "8.0.0", + "version": "9.1.0", "private": true, "dependencies": { "@floating-ui/react-dom": "1.0.0", "@floating-ui/react-dom-interactions": "0.10.3", + "@giphy/js-fetch-api": "5.1.0", + "@giphy/react-components": "8.1.0", "@guyplusplus/turndown-plugin-gfm": "1.0.7", "@mattermost/client": "*", "@mattermost/compass-components": "^0.2.12", - "@mattermost/compass-icons": "0.1.34", + "@mattermost/compass-icons": "0.1.37", "@mattermost/types": "*", "@mui/base": "5.0.0-alpha.127", "@mui/material": "5.11.16", @@ -38,7 +40,6 @@ "fast-deep-equal": "3.1.3", "flexsearch": "0.6.32", "font-awesome": "4.7.0", - "gfycat-sdk": "1.4.18", "highlight.js": "11.6.0", "history": "4.10.1", "hoist-non-react-statics": "3.3.2", @@ -101,10 +102,11 @@ "devDependencies": { "@deanwhillier/jest-matchmedia-mock": "1.2.0", "@hot-loader/react-dom": "17.0.2", + "@mattermost/eslint-plugin": "*", "@redux-devtools/extension": "3.2.3", "@testing-library/jest-dom": "5.16.4", "@testing-library/react": "12.1.4", - "@testing-library/user-event": "12.1.4", + "@testing-library/user-event": "13.5.0", "@types/bootstrap": "4.5.0", "@types/country-list": "2.1.0", "@types/enzyme": "3.10.11", @@ -147,7 +149,6 @@ "enzyme-to-json": "3.6.2", "eslint-plugin-header": "3.1.1", "eslint-plugin-import": "2.27.5", - "eslint-plugin-mattermost": "github:mattermost/eslint-plugin-mattermost#5b0c972eacf19286e4c66221b39113bf8728a99e", "eslint-plugin-no-only-tests": "3.1.0", "eslint-plugin-react": "7.32.2", "eslint-plugin-react-hooks": "4.6.0", diff --git a/webapp/channels/src/actions/admin_actions.jsx b/webapp/channels/src/actions/admin_actions.jsx index fc60696af9a..8831620e469 100644 --- a/webapp/channels/src/actions/admin_actions.jsx +++ b/webapp/channels/src/actions/admin_actions.jsx @@ -2,16 +2,16 @@ // See LICENSE.txt for license information. import * as AdminActions from 'mattermost-redux/actions/admin'; -import * as UserActions from 'mattermost-redux/actions/users'; -import * as TeamActions from 'mattermost-redux/actions/teams'; -import {Client4} from 'mattermost-redux/client'; import {bindClientFunc} from 'mattermost-redux/actions/helpers'; - -import {trackEvent} from 'actions/telemetry_actions.jsx'; +import * as TeamActions from 'mattermost-redux/actions/teams'; +import * as UserActions from 'mattermost-redux/actions/users'; +import {Client4} from 'mattermost-redux/client'; import {emitUserLoggedOutEvent} from 'actions/global_actions'; +import {trackEvent} from 'actions/telemetry_actions.jsx'; import {getOnNavigationConfirmed} from 'selectors/views/admin'; import store from 'stores/redux_store.jsx'; + import {ActionTypes} from 'utils/constants'; const dispatch = store.dispatch; diff --git a/webapp/channels/src/actions/apps.ts b/webapp/channels/src/actions/apps.ts index ae91dfa1028..863b88e01b9 100644 --- a/webapp/channels/src/actions/apps.ts +++ b/webapp/channels/src/actions/apps.ts @@ -1,23 +1,23 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. +import type {AppCallResponse, AppForm, AppCallRequest, AppContext, AppBinding} from '@mattermost/types/apps'; +import type {CommandArgs} from '@mattermost/types/integrations'; +import type {Post} from '@mattermost/types/posts'; + import {Client4} from 'mattermost-redux/client'; -import {Action, ActionFunc, DispatchFunc} from 'mattermost-redux/types/actions'; -import {AppCallResponse, AppForm, AppCallRequest, AppContext, AppBinding} from '@mattermost/types/apps'; import {AppCallResponseTypes} from 'mattermost-redux/constants/apps'; -import {Post} from '@mattermost/types/posts'; -import {CommandArgs} from '@mattermost/types/integrations'; +import type {Action, ActionFunc, DispatchFunc} from 'mattermost-redux/types/actions'; +import {cleanForm} from 'mattermost-redux/utils/apps'; import {openModal} from 'actions/views/modals'; import AppsForm from 'components/apps_form'; +import {createCallRequest, makeCallErrorResponse} from 'utils/apps'; +import {getHistory} from 'utils/browser_history'; import {ModalIdentifiers} from 'utils/constants'; import {getSiteURL, shouldOpenInNewTab} from 'utils/url'; -import {getHistory} from 'utils/browser_history'; -import {createCallRequest, makeCallErrorResponse} from 'utils/apps'; - -import {cleanForm} from 'mattermost-redux/utils/apps'; import {sendEphemeralPost} from './global_actions'; @@ -114,9 +114,7 @@ export function doAppSubmit(inCall: AppCallRequest, intl: any): Act window.open(res.navigate_to_url); return {data: res}; } - const navigateURL = res.navigate_to_url.startsWith(getSiteURL()) ? - res.navigate_to_url.slice(getSiteURL().length) : - res.navigate_to_url; + const navigateURL = res.navigate_to_url.startsWith(getSiteURL()) ? res.navigate_to_url.slice(getSiteURL().length) : res.navigate_to_url; getHistory().push(navigateURL); return {data: res}; } diff --git a/webapp/channels/src/actions/channel_actions.test.ts b/webapp/channels/src/actions/channel_actions.test.ts index aac40512ec9..d13d73782b0 100644 --- a/webapp/channels/src/actions/channel_actions.test.ts +++ b/webapp/channels/src/actions/channel_actions.test.ts @@ -3,6 +3,13 @@ import nock from 'nock'; +import type {Channel} from '@mattermost/types/channels'; +import type {Role} from '@mattermost/types/roles'; +import type {Team} from '@mattermost/types/teams'; +import type {UserProfile} from '@mattermost/types/users'; + +import {Client4} from 'mattermost-redux/client'; + import { searchMoreChannels, addUsersToChannel, @@ -10,18 +17,12 @@ import { openGroupChannelToUserIds, loadChannelsForCurrentUser, fetchChannelsAndMembers, } from 'actions/channel_actions'; -import {loadProfilesForSidebar} from 'actions/user_actions'; import {CHANNELS_AND_CHANNEL_MEMBERS_PER_PAGE} from 'actions/channel_queries'; +import {loadProfilesForSidebar} from 'actions/user_actions'; +import configureStore from 'store'; -import {Channel} from '@mattermost/types/channels'; -import {Team} from '@mattermost/types/teams'; -import {UserProfile} from '@mattermost/types/users'; -import {Role} from '@mattermost/types/roles'; - -import {Client4} from 'mattermost-redux/client'; import TestHelper from 'packages/mattermost-redux/test/test_helper'; import mockStore from 'tests/test_store'; -import configureStore from 'store'; const initialState = { entities: { diff --git a/webapp/channels/src/actions/channel_actions.ts b/webapp/channels/src/actions/channel_actions.ts index 0add0175140..629ea07c8f0 100644 --- a/webapp/channels/src/actions/channel_actions.ts +++ b/webapp/channels/src/actions/channel_actions.ts @@ -3,34 +3,35 @@ import {batchActions} from 'redux-batched-actions'; -import {UserProfile} from '@mattermost/types/users'; -import {Channel, ChannelMembership, ServerChannel} from '@mattermost/types/channels'; -import {Team} from '@mattermost/types/teams'; -import {ServerError} from '@mattermost/types/errors'; -import {Role} from '@mattermost/types/roles'; +import type {Channel, ChannelMembership, ServerChannel} from '@mattermost/types/channels'; +import type {ServerError} from '@mattermost/types/errors'; +import type {Role} from '@mattermost/types/roles'; +import type {Team} from '@mattermost/types/teams'; +import type {UserProfile} from '@mattermost/types/users'; -import {Client4} from 'mattermost-redux/client'; import {ChannelTypes, PreferenceTypes, RoleTypes} from 'mattermost-redux/action_types'; import * as ChannelActions from 'mattermost-redux/actions/channels'; -import {savePreferences} from 'mattermost-redux/actions/preferences'; -import {ActionFunc} from 'mattermost-redux/types/actions'; import {logError} from 'mattermost-redux/actions/errors'; -import {getMyChannelMemberships} from 'mattermost-redux/selectors/entities/common'; +import {savePreferences} from 'mattermost-redux/actions/preferences'; +import {Client4} from 'mattermost-redux/client'; import {getChannelByName, getUnreadChannelIds, getChannel} from 'mattermost-redux/selectors/entities/channels'; +import {getMyChannelMemberships} from 'mattermost-redux/selectors/entities/common'; import {getCurrentTeamUrl, getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams'; import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users'; +import type {ActionFunc} from 'mattermost-redux/types/actions'; -import {trackEvent} from 'actions/telemetry_actions.jsx'; -import {loadNewDMIfNeeded, loadNewGMIfNeeded, loadProfilesForSidebar} from 'actions/user_actions'; import { getChannelsAndChannelMembersQueryString, transformToReceivedChannelsReducerPayload, transformToReceivedChannelMembersReducerPayload, - ChannelsAndChannelMembersQueryResponseType, - GraphQLChannel, - GraphQLChannelMember, CHANNELS_AND_CHANNEL_MEMBERS_PER_PAGE, } from 'actions/channel_queries'; +import type { + ChannelsAndChannelMembersQueryResponseType, + GraphQLChannel, + GraphQLChannelMember} from 'actions/channel_queries'; +import {trackEvent} from 'actions/telemetry_actions.jsx'; +import {loadNewDMIfNeeded, loadNewGMIfNeeded, loadProfilesForSidebar} from 'actions/user_actions'; import {getHistory} from 'utils/browser_history'; import {Constants, Preferences, NotificationLevels} from 'utils/constants'; diff --git a/webapp/channels/src/actions/channel_queries.ts b/webapp/channels/src/actions/channel_queries.ts index d3a9d7f1f72..2f6ffcb68c3 100644 --- a/webapp/channels/src/actions/channel_queries.ts +++ b/webapp/channels/src/actions/channel_queries.ts @@ -1,10 +1,10 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import {ChannelMembership, ServerChannel, ChannelType} from '@mattermost/types/channels'; -import {Role} from '@mattermost/types/roles'; -import {Team} from '@mattermost/types/teams'; -import {UserProfile} from '@mattermost/types/users'; +import type {ChannelMembership, ServerChannel, ChannelType} from '@mattermost/types/channels'; +import type {Role} from '@mattermost/types/roles'; +import type {Team} from '@mattermost/types/teams'; +import type {UserProfile} from '@mattermost/types/users'; import {convertRolesNamesArrayToString} from 'mattermost-redux/actions/roles'; diff --git a/webapp/channels/src/actions/cloud.tsx b/webapp/channels/src/actions/cloud.tsx index ff9e83f3369..79489ce5f99 100644 --- a/webapp/channels/src/actions/cloud.tsx +++ b/webapp/channels/src/actions/cloud.tsx @@ -1,22 +1,24 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import {Stripe} from '@stripe/stripe-js'; +import type {Stripe} from '@stripe/stripe-js'; import {getCode} from 'country-list'; +import type {Address, CloudCustomerPatch, Feedback, WorkspaceDeletionRequest} from '@mattermost/types/cloud'; + +import {CloudTypes} from 'mattermost-redux/action_types'; import {getCloudCustomer, getCloudProducts, getCloudSubscription, getInvoices} from 'mattermost-redux/actions/cloud'; import {Client4} from 'mattermost-redux/client'; import {getCloudErrors} from 'mattermost-redux/selectors/entities/cloud'; -import {ActionFunc, DispatchFunc, GetStateFunc} from 'mattermost-redux/types/actions'; - -import {getConfirmCardSetup} from 'components/payment_form/stripe'; +import type {ActionFunc, DispatchFunc, GetStateFunc} from 'mattermost-redux/types/actions'; import {trackEvent} from 'actions/telemetry_actions.jsx'; -import {StripeSetupIntent, BillingDetails} from 'types/cloud/sku'; -import {CloudTypes} from 'mattermost-redux/action_types'; +import {getConfirmCardSetup} from 'components/payment_form/stripe'; + import {getBlankAddressWithCountry} from 'utils/utils'; -import {Address, Feedback, WorkspaceDeletionRequest} from '@mattermost/types/cloud'; + +import type {StripeSetupIntent, BillingDetails} from 'types/cloud/sku'; // Returns true for success, and false for any error export function completeStripeAddPaymentMethod( @@ -87,6 +89,7 @@ export function subscribeCloudSubscription( shippingAddress: Address = getBlankAddressWithCountry(), seats = 0, downgradeFeedback?: Feedback, + customerPatch?: CloudCustomerPatch, ) { return async () => { try { @@ -95,6 +98,7 @@ export function subscribeCloudSubscription( shippingAddress, seats, downgradeFeedback, + customerPatch, ); return {data: subscription}; diff --git a/webapp/channels/src/actions/command.test.js b/webapp/channels/src/actions/command.test.js index de9a9b01be6..e35058051a2 100644 --- a/webapp/channels/src/actions/command.test.js +++ b/webapp/channels/src/actions/command.test.js @@ -2,23 +2,20 @@ // See LICENSE.txt for license information. import {Client4} from 'mattermost-redux/client'; - +import {Permissions} from 'mattermost-redux/constants'; +import {AppCallResponseTypes} from 'mattermost-redux/constants/apps'; import * as Channels from 'mattermost-redux/selectors/entities/channels'; import * as Teams from 'mattermost-redux/selectors/entities/teams'; -import {Permissions} from 'mattermost-redux/constants'; -import {AppCallResponseTypes} from 'mattermost-redux/constants/apps'; - import * as GlobalActions from 'actions/global_actions'; -import mockStore from 'tests/test_store'; +import UserSettingsModal from 'components/user_settings/modal'; +import mockStore from 'tests/test_store'; import {ActionTypes, Constants, ModalIdentifiers} from 'utils/constants'; import * as UserAgent from 'utils/user_agent'; import * as Utils from 'utils/utils'; -import UserSettingsModal from 'components/user_settings/modal'; - import {executeCommand} from './command'; const currentChannelId = '123'; diff --git a/webapp/channels/src/actions/command.ts b/webapp/channels/src/actions/command.ts index 2825b996ba2..c1bd3a5edb1 100644 --- a/webapp/channels/src/actions/command.ts +++ b/webapp/channels/src/actions/command.ts @@ -1,43 +1,41 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import {Client4} from 'mattermost-redux/client'; -import {unfavoriteChannel} from 'mattermost-redux/actions/channels'; -import {savePreferences} from 'mattermost-redux/actions/preferences'; -import {getCurrentChannel, getRedirectChannelNameForTeam, isFavoriteChannel} from 'mattermost-redux/selectors/entities/channels'; -import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users'; -import {getCurrentRelativeTeamUrl, getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams'; -import {appsEnabled} from 'mattermost-redux/selectors/entities/apps'; -import {IntegrationTypes} from 'mattermost-redux/action_types'; -import {ActionFunc, DispatchFunc, GetStateFunc} from 'mattermost-redux/types/actions'; import type {CommandArgs} from '@mattermost/types/integrations'; +import {IntegrationTypes} from 'mattermost-redux/action_types'; +import {unfavoriteChannel} from 'mattermost-redux/actions/channels'; +import {savePreferences} from 'mattermost-redux/actions/preferences'; +import {Client4} from 'mattermost-redux/client'; +import {Permissions} from 'mattermost-redux/constants'; import {AppCallResponseTypes} from 'mattermost-redux/constants/apps'; +import {appsEnabled} from 'mattermost-redux/selectors/entities/apps'; +import {getCurrentChannel, getRedirectChannelNameForTeam, isFavoriteChannel} from 'mattermost-redux/selectors/entities/channels'; +import {isMarketplaceEnabled} from 'mattermost-redux/selectors/entities/general'; +import {haveICurrentTeamPermission} from 'mattermost-redux/selectors/entities/roles'; +import {getCurrentRelativeTeamUrl, getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams'; +import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users'; +import type {ActionFunc, DispatchFunc, GetStateFunc} from 'mattermost-redux/types/actions'; -import {DoAppCallResult} from 'types/apps'; - -import {openModal} from 'actions/views/modals'; import * as GlobalActions from 'actions/global_actions'; import * as PostActions from 'actions/post_actions'; +import {openModal} from 'actions/views/modals'; -import {isUrlSafe, getSiteURL} from 'utils/url'; -import {localizeMessage, getUserIdFromChannelName} from 'utils/utils'; -import * as UserAgent from 'utils/user_agent'; -import {Constants, ModalIdentifiers} from 'utils/constants'; -import {getHistory} from 'utils/browser_history'; - -import UserSettingsModal from 'components/user_settings/modal'; +import KeyboardShortcutsModal from 'components/keyboard_shortcuts/keyboard_shortcuts_modal/keyboard_shortcuts_modal'; +import LeaveChannelModal from 'components/leave_channel_modal'; +import MarketplaceModal from 'components/plugin_marketplace/marketplace_modal'; import {AppCommandParser} from 'components/suggestion/command_provider/app_command_parser/app_command_parser'; import {intlShim} from 'components/suggestion/command_provider/app_command_parser/app_command_parser_dependencies'; -import LeaveChannelModal from 'components/leave_channel_modal'; -import KeyboardShortcutsModal from 'components/keyboard_shortcuts/keyboard_shortcuts_modal/keyboard_shortcuts_modal'; +import UserSettingsModal from 'components/user_settings/modal'; -import {GlobalState} from 'types/store'; +import {getHistory} from 'utils/browser_history'; +import {Constants, ModalIdentifiers} from 'utils/constants'; +import {isUrlSafe, getSiteURL} from 'utils/url'; +import * as UserAgent from 'utils/user_agent'; +import {localizeMessage, getUserIdFromChannelName} from 'utils/utils'; -import MarketplaceModal from 'components/plugin_marketplace/marketplace_modal'; -import {haveICurrentTeamPermission} from 'mattermost-redux/selectors/entities/roles'; -import {Permissions} from 'mattermost-redux/constants'; -import {isMarketplaceEnabled} from 'mattermost-redux/selectors/entities/general'; +import type {DoAppCallResult} from 'types/apps'; +import type {GlobalState} from 'types/store'; import {doAppSubmit, openAppsModal, postEphemeralCallResponseForCommandArgs} from './apps'; import {trackEvent} from './telemetry_actions'; diff --git a/webapp/channels/src/actions/emoji_actions.js b/webapp/channels/src/actions/emoji_actions.js index f704572b82c..0e38996d684 100644 --- a/webapp/channels/src/actions/emoji_actions.js +++ b/webapp/channels/src/actions/emoji_actions.js @@ -2,14 +2,13 @@ // See LICENSE.txt for license information. import * as EmojiActions from 'mattermost-redux/actions/emojis'; +import {savePreferences} from 'mattermost-redux/actions/preferences'; import {getCustomEmojisByName} from 'mattermost-redux/selectors/entities/emojis'; import {getConfig} from 'mattermost-redux/selectors/entities/general'; import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users'; import {getEmojiMap, getRecentEmojisData, getRecentEmojisNames, isCustomEmojiEnabled} from 'selectors/emojis'; import {isCustomStatusEnabled, makeGetCustomStatus} from 'selectors/views/custom_status'; -import {savePreferences} from 'mattermost-redux/actions/preferences'; - import LocalStorageStore from 'stores/local_storage_store'; import Constants, {ActionTypes, Preferences} from 'utils/constants'; diff --git a/webapp/channels/src/actions/emoji_actions.test.js b/webapp/channels/src/actions/emoji_actions.test.js index 741ed3cf455..2406fa5988a 100644 --- a/webapp/channels/src/actions/emoji_actions.test.js +++ b/webapp/channels/src/actions/emoji_actions.test.js @@ -1,10 +1,11 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import {getRecentEmojisData, getEmojiMap} from 'selectors/emojis'; -import * as EmojiActions from 'actions/emoji_actions'; import * as PreferenceActions from 'mattermost-redux/actions/preferences'; +import * as EmojiActions from 'actions/emoji_actions'; +import {getRecentEmojisData, getEmojiMap} from 'selectors/emojis'; + import mockStore from 'tests/test_store'; const currentUserId = 'current_user_id'; diff --git a/webapp/channels/src/actions/file_actions.ts b/webapp/channels/src/actions/file_actions.ts index 0804ed3c1dd..a9101b232fb 100644 --- a/webapp/channels/src/actions/file_actions.ts +++ b/webapp/channels/src/actions/file_actions.ts @@ -3,15 +3,16 @@ import {batchActions} from 'redux-batched-actions'; -import {FileInfo} from '@mattermost/types/files'; -import {ServerError} from '@mattermost/types/errors'; +import type {ServerError} from '@mattermost/types/errors'; +import type {FileInfo} from '@mattermost/types/files'; import {FileTypes} from 'mattermost-redux/action_types'; import {getLogErrorAction} from 'mattermost-redux/actions/errors'; import {forceLogoutIfNecessary} from 'mattermost-redux/actions/helpers'; -import {DispatchFunc, GetStateFunc} from 'mattermost-redux/types/actions'; import {Client4} from 'mattermost-redux/client'; -import {FilePreviewInfo} from 'components/file_preview/file_preview'; +import type {DispatchFunc, GetStateFunc} from 'mattermost-redux/types/actions'; + +import type {FilePreviewInfo} from 'components/file_preview/file_preview'; import {localizeMessage} from 'utils/utils'; @@ -92,8 +93,7 @@ export function uploadFile({file, name, type, rootId, channelId, clientId, onPro try { const errorResponse = JSON.parse(xhr.response); errorMessage = - (errorResponse?.id && errorResponse?.message) ? localizeMessage(errorResponse.id, errorResponse.message) : - localizeMessage('file_upload.generic_error', 'There was a problem uploading your files.'); + (errorResponse?.id && errorResponse?.message) ? localizeMessage(errorResponse.id, errorResponse.message) : localizeMessage('file_upload.generic_error', 'There was a problem uploading your files.'); } catch (e) { errorMessage = localizeMessage('file_upload.generic_error', 'There was a problem uploading your files.'); } @@ -128,9 +128,7 @@ export function uploadFile({file, name, type, rootId, channelId, clientId, onPro dispatch(batchActions([uploadFailureAction, getLogErrorAction(errorResponse)])); onError(errorResponse, clientId, channelId, rootId); } else { - const errorMessage = xhr.status === 0 || !xhr.status ? - localizeMessage('file_upload.generic_error', 'There was a problem uploading your files.') : - localizeMessage('channel_loader.unknown_error', 'We received an unexpected status code from the server.') + ' (' + xhr.status + ')'; + const errorMessage = xhr.status === 0 || !xhr.status ? localizeMessage('file_upload.generic_error', 'There was a problem uploading your files.') : localizeMessage('channel_loader.unknown_error', 'We received an unexpected status code from the server.') + ' (' + xhr.status + ')'; dispatch({ type: FileTypes.UPLOAD_FILES_FAILURE, diff --git a/webapp/channels/src/actions/global_actions.test.ts b/webapp/channels/src/actions/global_actions.test.ts index 962eba9a6e4..cc3d2097d1d 100644 --- a/webapp/channels/src/actions/global_actions.test.ts +++ b/webapp/channels/src/actions/global_actions.test.ts @@ -1,17 +1,17 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import {UserProfile} from '@mattermost/types/users'; -import {Team} from '@mattermost/types/teams'; +import type {Team} from '@mattermost/types/teams'; +import type {UserProfile} from '@mattermost/types/users'; -import {getHistory} from 'utils/browser_history'; -import {closeRightHandSide, closeMenu as closeRhsMenu} from 'actions/views/rhs'; +import {redirectUserToDefaultTeam, toggleSideBarRightMenuAction, getTeamRedirectChannelIfIsAccesible} from 'actions/global_actions'; import {close as closeLhs} from 'actions/views/lhs'; +import {closeRightHandSide, closeMenu as closeRhsMenu} from 'actions/views/rhs'; import LocalStorageStore from 'stores/local_storage_store'; import reduxStore from 'stores/redux_store'; -import {redirectUserToDefaultTeam, toggleSideBarRightMenuAction, getTeamRedirectChannelIfIsAccesible} from 'actions/global_actions'; import mockStore from 'tests/test_store'; +import {getHistory} from 'utils/browser_history'; jest.mock('actions/views/rhs', () => ({ closeMenu: jest.fn(), diff --git a/webapp/channels/src/actions/global_actions.tsx b/webapp/channels/src/actions/global_actions.tsx index 7f05e891775..147629773f8 100644 --- a/webapp/channels/src/actions/global_actions.tsx +++ b/webapp/channels/src/actions/global_actions.tsx @@ -3,6 +3,13 @@ import {batchActions} from 'redux-batched-actions'; +import type {Channel, ChannelMembership} from '@mattermost/types/channels'; +import type {Post} from '@mattermost/types/posts'; +import type {Team} from '@mattermost/types/teams'; +import type {UserProfile} from '@mattermost/types/users'; + +import {ChannelTypes} from 'mattermost-redux/action_types'; +import {fetchAppBindings} from 'mattermost-redux/actions/apps'; import { fetchMyChannelsAndMembersREST, getChannelByNameAndTeamName, @@ -11,42 +18,37 @@ import { } from 'mattermost-redux/actions/channels'; import {logout, loadMe, loadMeREST} from 'mattermost-redux/actions/users'; import {Preferences} from 'mattermost-redux/constants'; -import {getConfig, isPerformanceDebuggingEnabled} from 'mattermost-redux/selectors/entities/general'; -import {getCurrentTeamId, getMyTeams, getTeam, getMyTeamMember, getTeamMemberships, getActiveTeamsList} from 'mattermost-redux/selectors/entities/teams'; -import {getBool, getIsOnboardingFlowEnabled, isCollapsedThreadsEnabled, isGraphQLEnabled} from 'mattermost-redux/selectors/entities/preferences'; -import {getCurrentUser, getCurrentUserId, isFirstAdmin} from 'mattermost-redux/selectors/entities/users'; -import {getCurrentChannelStats, getCurrentChannelId, getMyChannelMember, getRedirectChannelNameForTeam, getChannelsNameMapInTeam, getAllDirectChannels, getChannelMessageCount} from 'mattermost-redux/selectors/entities/channels'; import {appsEnabled} from 'mattermost-redux/selectors/entities/apps'; -import {ChannelTypes} from 'mattermost-redux/action_types'; -import {fetchAppBindings} from 'mattermost-redux/actions/apps'; -import {Channel, ChannelMembership} from '@mattermost/types/channels'; -import {UserProfile} from '@mattermost/types/users'; -import {Post} from '@mattermost/types/posts'; -import {ActionFunc, DispatchFunc, GetStateFunc} from 'mattermost-redux/types/actions'; -import {Team} from '@mattermost/types/teams'; +import {getCurrentChannelStats, getCurrentChannelId, getMyChannelMember, getRedirectChannelNameForTeam, getChannelsNameMapInTeam, getAllDirectChannels, getChannelMessageCount} from 'mattermost-redux/selectors/entities/channels'; +import {getConfig, isPerformanceDebuggingEnabled} from 'mattermost-redux/selectors/entities/general'; +import {getBool, getIsOnboardingFlowEnabled, isCollapsedThreadsEnabled, isGraphQLEnabled} from 'mattermost-redux/selectors/entities/preferences'; +import {getCurrentTeamId, getMyTeams, getTeam, getMyTeamMember, getTeamMemberships, getActiveTeamsList} from 'mattermost-redux/selectors/entities/teams'; +import {getCurrentUser, getCurrentUserId, isFirstAdmin} from 'mattermost-redux/selectors/entities/users'; +import type {ActionFunc, DispatchFunc, GetStateFunc} from 'mattermost-redux/types/actions'; import {calculateUnreadCount} from 'mattermost-redux/utils/channel_utils'; -import {getHistory} from 'utils/browser_history'; import {handleNewPost} from 'actions/post_actions'; import {stopPeriodicStatusUpdates} from 'actions/status_actions'; import {loadProfilesForSidebar} from 'actions/user_actions'; -import {closeRightHandSide, closeMenu as closeRhsMenu, updateRhsState} from 'actions/views/rhs'; import {clearUserCookie} from 'actions/views/cookie'; import {close as closeLhs} from 'actions/views/lhs'; +import {closeRightHandSide, closeMenu as closeRhsMenu, updateRhsState} from 'actions/views/rhs'; import * as WebsocketActions from 'actions/websocket_actions.jsx'; import {getCurrentLocale} from 'selectors/i18n'; import {getIsRhsOpen, getPreviousRhsState, getRhsState} from 'selectors/rhs'; import BrowserStore from 'stores/browser_store'; -import store from 'stores/redux_store.jsx'; import LocalStorageStore from 'stores/local_storage_store'; +import store from 'stores/redux_store.jsx'; + +import SubMenuModal from 'components/widgets/menu/menu_modals/submenu_modal/submenu_modal'; + import WebSocketClient from 'client/web_websocket_client.jsx'; - -import {GlobalState} from 'types/store'; - +import {getHistory} from 'utils/browser_history'; import {ActionTypes, PostTypes, RHSStates, ModalIdentifiers, PreviousViewedTypes} from 'utils/constants'; import {filterAndSortTeamsByDisplayName} from 'utils/team_utils'; import * as Utils from 'utils/utils'; -import SubMenuModal from '../components/widgets/menu/menu_modals/submenu_modal/submenu_modal'; + +import type {GlobalState} from 'types/store'; import {openModal} from './views/modals'; diff --git a/webapp/channels/src/actions/hooks.test.js b/webapp/channels/src/actions/hooks.test.js index 125f0349ce3..971b8532ebc 100644 --- a/webapp/channels/src/actions/hooks.test.js +++ b/webapp/channels/src/actions/hooks.test.js @@ -2,6 +2,7 @@ // See LICENSE.txt for license information. import {General} from 'mattermost-redux/constants'; + import mockStore from 'tests/test_store'; import { diff --git a/webapp/channels/src/actions/hosted_customer.tsx b/webapp/channels/src/actions/hosted_customer.tsx index 4e7d1d6ad8b..0a6a3743da5 100644 --- a/webapp/channels/src/actions/hosted_customer.tsx +++ b/webapp/channels/src/actions/hosted_customer.tsx @@ -1,22 +1,23 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import {Stripe} from '@stripe/stripe-js'; - +import type {Stripe} from '@stripe/stripe-js'; import {getCode} from 'country-list'; -import {CreateSubscriptionRequest} from '@mattermost/types/cloud'; -import {SelfHostedExpansionRequest, SelfHostedSignupProgress} from '@mattermost/types/hosted_customer'; -import {ValueOf} from '@mattermost/types/utilities'; +import type {CreateSubscriptionRequest} from '@mattermost/types/cloud'; +import type {SelfHostedExpansionRequest} from '@mattermost/types/hosted_customer'; +import {SelfHostedSignupProgress} from '@mattermost/types/hosted_customer'; +import type {ValueOf} from '@mattermost/types/utilities'; -import {Client4} from 'mattermost-redux/client'; import {HostedCustomerTypes} from 'mattermost-redux/action_types'; -import {ActionFunc, DispatchFunc, GetStateFunc} from 'mattermost-redux/types/actions'; +import {bindClientFunc} from 'mattermost-redux/actions/helpers'; +import {Client4} from 'mattermost-redux/client'; import {getSelfHostedErrors} from 'mattermost-redux/selectors/entities/hosted_customer'; -import {StripeSetupIntent, BillingDetails} from 'types/cloud/sku'; +import type {ActionFunc, DispatchFunc, GetStateFunc} from 'mattermost-redux/types/actions'; import {getConfirmCardSetup} from 'components/payment_form/stripe'; -import {bindClientFunc} from 'mattermost-redux/actions/helpers'; + +import type {StripeSetupIntent, BillingDetails} from 'types/cloud/sku'; function selfHostedNeedsConfirmation(progress: ValueOf): boolean { switch (progress) { diff --git a/webapp/channels/src/actions/integration_actions.jsx b/webapp/channels/src/actions/integration_actions.jsx index 0d996ee216f..d8a4b384ccf 100644 --- a/webapp/channels/src/actions/integration_actions.jsx +++ b/webapp/channels/src/actions/integration_actions.jsx @@ -3,8 +3,8 @@ import * as IntegrationActions from 'mattermost-redux/actions/integrations'; import {getProfilesByIds} from 'mattermost-redux/actions/users'; -import {getUser} from 'mattermost-redux/selectors/entities/users'; import {appsEnabled} from 'mattermost-redux/selectors/entities/apps'; +import {getUser} from 'mattermost-redux/selectors/entities/users'; const DEFAULT_PAGE_SIZE = 100; diff --git a/webapp/channels/src/actions/integration_actions.test.js b/webapp/channels/src/actions/integration_actions.test.js index 3b253b044d2..be9328ccefd 100644 --- a/webapp/channels/src/actions/integration_actions.test.js +++ b/webapp/channels/src/actions/integration_actions.test.js @@ -3,10 +3,10 @@ import {getProfilesByIds} from 'mattermost-redux/actions/users'; -import mockStore from 'tests/test_store'; - import * as Actions from 'actions/integration_actions.jsx'; +import mockStore from 'tests/test_store'; + jest.mock('mattermost-redux/actions/users', () => ({ getProfilesByIds: jest.fn(() => { return {type: ''}; diff --git a/webapp/channels/src/actions/invite_actions.test.ts b/webapp/channels/src/actions/invite_actions.test.ts index e38a9be1130..caf7e342b51 100644 --- a/webapp/channels/src/actions/invite_actions.test.ts +++ b/webapp/channels/src/actions/invite_actions.test.ts @@ -1,14 +1,15 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import {Channel} from '@mattermost/types/channels'; -import {UserProfile} from '@mattermost/types/users'; +import type {Channel} from '@mattermost/types/channels'; +import type {UserProfile} from '@mattermost/types/users'; -import {DispatchFunc, GetStateFunc} from 'mattermost-redux/types/actions'; +import type {DispatchFunc, GetStateFunc} from 'mattermost-redux/types/actions'; import {sendMembersInvites, sendGuestsInvites} from 'actions/invite_actions'; + import mockStore from 'tests/test_store'; -import {ConsolePages} from '../utils/constants'; +import {ConsolePages} from 'utils/constants'; jest.mock('actions/team_actions', () => ({ addUsersToTeam: () => ({ // since we are using addUsersToTeamGracefully, this call will always succeed diff --git a/webapp/channels/src/actions/invite_actions.ts b/webapp/channels/src/actions/invite_actions.ts index 5291d03c5af..e31e8020401 100644 --- a/webapp/channels/src/actions/invite_actions.ts +++ b/webapp/channels/src/actions/invite_actions.ts @@ -1,23 +1,24 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import {RelationOneToOne} from '@mattermost/types/utilities'; -import {UserProfile} from '@mattermost/types/users'; -import {Channel, ChannelMembership} from '@mattermost/types/channels'; -import {TeamMemberWithError, TeamInviteWithError} from '@mattermost/types/teams'; +import type {Channel, ChannelMembership} from '@mattermost/types/channels'; +import type {TeamMemberWithError, TeamInviteWithError} from '@mattermost/types/teams'; +import type {UserProfile} from '@mattermost/types/users'; +import type {RelationOneToOne} from '@mattermost/types/utilities'; -import {ActionFunc, DispatchFunc, GetStateFunc} from 'mattermost-redux/types/actions'; -import * as TeamActions from 'mattermost-redux/actions/teams'; import {joinChannel} from 'mattermost-redux/actions/channels'; -import {getTeamMember} from 'mattermost-redux/selectors/entities/teams'; +import * as TeamActions from 'mattermost-redux/actions/teams'; import {getChannelMembersInChannels} from 'mattermost-redux/selectors/entities/channels'; +import {getTeamMember} from 'mattermost-redux/selectors/entities/teams'; import {isCurrentUserSystemAdmin} from 'mattermost-redux/selectors/entities/users'; +import type {ActionFunc, DispatchFunc, GetStateFunc} from 'mattermost-redux/types/actions'; import {isGuest} from 'mattermost-redux/utils/user_utils'; import {addUsersToTeam} from 'actions/team_actions'; + +import {ConsolePages} from 'utils/constants'; import {t} from 'utils/i18n'; import {localizeMessage} from 'utils/utils'; -import {ConsolePages} from 'utils/constants'; export function sendMembersInvites(teamId: string, users: UserProfile[], emails: string[]): ActionFunc { return async (dispatch: DispatchFunc, getState: GetStateFunc) => { diff --git a/webapp/channels/src/actions/marketplace.ts b/webapp/channels/src/actions/marketplace.ts index f462760c33e..0c9cea536b2 100644 --- a/webapp/channels/src/actions/marketplace.ts +++ b/webapp/channels/src/actions/marketplace.ts @@ -1,24 +1,25 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import {Client4} from 'mattermost-redux/client'; - -import {getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams'; -import {getCurrentChannelId} from 'mattermost-redux/selectors/entities/channels'; -import {appsEnabled} from 'mattermost-redux/selectors/entities/apps'; - -import {ActionFunc, DispatchFunc, GetStateFunc} from 'mattermost-redux/types/actions'; +import type {AppCall, AppExpand, AppFormValues} from '@mattermost/types/apps'; import type {MarketplaceApp, MarketplacePlugin} from '@mattermost/types/marketplace'; -import {GlobalState} from 'types/store'; +import {Client4} from 'mattermost-redux/client'; +import {AppBindingLocations, AppCallResponseTypes} from 'mattermost-redux/constants/apps'; +import {appsEnabled} from 'mattermost-redux/selectors/entities/apps'; +import {getCurrentChannelId} from 'mattermost-redux/selectors/entities/channels'; +import {getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams'; +import type {ActionFunc, DispatchFunc, GetStateFunc} from 'mattermost-redux/types/actions'; import {getFilter, getPlugin} from 'selectors/views/marketplace'; + +import {intlShim} from 'components/suggestion/command_provider/app_command_parser/app_command_parser_dependencies'; +import type {DoAppCallResult} from 'components/suggestion/command_provider/app_command_parser/app_command_parser_dependencies'; + +import {createCallContext, createCallRequest} from 'utils/apps'; import {ActionTypes} from 'utils/constants'; -import {AppBindingLocations, AppCallResponseTypes} from 'mattermost-redux/constants/apps'; -import {AppCall, AppExpand, AppFormValues} from '@mattermost/types/apps'; -import {createCallContext, createCallRequest} from 'utils/apps'; -import {DoAppCallResult, intlShim} from 'components/suggestion/command_provider/app_command_parser/app_command_parser_dependencies'; +import type {GlobalState} from 'types/store'; import {doAppSubmit, openAppsModal, postEphemeralCallResponseForContext} from './apps'; diff --git a/webapp/channels/src/actions/new_post.test.ts b/webapp/channels/src/actions/new_post.test.ts index 2ef6a7d7d5a..9bfb30f69bf 100644 --- a/webapp/channels/src/actions/new_post.test.ts +++ b/webapp/channels/src/actions/new_post.test.ts @@ -1,21 +1,18 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -// import thunk from 'redux-thunk'; -// import configureStore from 'redux-mock-store'; - -import {Post} from '@mattermost/types/posts'; +import type {Post} from '@mattermost/types/posts'; +import type {GlobalState} from '@mattermost/types/store'; import {ChannelTypes} from 'mattermost-redux/action_types'; import {receivedNewPost} from 'mattermost-redux/actions/posts'; import {Posts} from 'mattermost-redux/constants'; +import type {GetStateFunc} from 'mattermost-redux/types/actions'; import * as NewPostActions from 'actions/new_post'; + import mockStore from 'tests/test_store'; import {Constants} from 'utils/constants'; -import {GlobalState} from '@mattermost/types/store'; - -import {GetStateFunc} from 'mattermost-redux/types/actions'; jest.mock('mattermost-redux/actions/channels', () => ({ ...jest.requireActual('mattermost-redux/actions/channels'), @@ -174,7 +171,7 @@ describe('actions/new_post', () => { }, }, { - type: ChannelTypes.RECEIVED_MY_CHANNEL_MEMBER, + type: ChannelTypes.RECEIVED_LAST_VIEWED_AT, data: { channel_id: channelId, }, @@ -377,7 +374,7 @@ describe('actions/new_post', () => { }, }, { - type: ChannelTypes.RECEIVED_MY_CHANNEL_MEMBER, + type: ChannelTypes.RECEIVED_LAST_VIEWED_AT, data: { channel_id: channelId, }, diff --git a/webapp/channels/src/actions/new_post.ts b/webapp/channels/src/actions/new_post.ts index 8a0be88e600..7c0186c6312 100644 --- a/webapp/channels/src/actions/new_post.ts +++ b/webapp/channels/src/actions/new_post.ts @@ -1,40 +1,36 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import * as Redux from 'redux'; +import type * as Redux from 'redux'; import {batchActions} from 'redux-batched-actions'; -import {isCollapsedThreadsEnabled} from 'mattermost-redux/selectors/entities/preferences'; +import type {Post} from '@mattermost/types/posts'; import { actionsToMarkChannelAsRead, actionsToMarkChannelAsUnread, - actionsToMarkChannelAsViewed, markChannelAsViewedOnServer, } from 'mattermost-redux/actions/channels'; import * as PostActions from 'mattermost-redux/actions/posts'; - import {getCurrentChannelId, isManuallyUnread} from 'mattermost-redux/selectors/entities/channels'; import * as PostSelectors from 'mattermost-redux/selectors/entities/posts'; -import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users'; +import {isCollapsedThreadsEnabled} from 'mattermost-redux/selectors/entities/preferences'; import {getThread} from 'mattermost-redux/selectors/entities/threads'; - -import {ActionFunc, DispatchFunc, GetStateFunc} from 'mattermost-redux/types/actions'; -import {Post} from '@mattermost/types/posts'; - +import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users'; +import type {ActionFunc, DispatchFunc, GetStateFunc} from 'mattermost-redux/types/actions'; import { isFromWebhook, isSystemMessage, shouldIgnorePost, } from 'mattermost-redux/utils/post_utils'; -import {GlobalState} from 'types/store'; - -import {updateThreadLastOpened} from 'actions/views/threads'; import {sendDesktopNotification} from 'actions/notification_actions.jsx'; +import {updateThreadLastOpened} from 'actions/views/threads'; +import {isThreadOpen, makeGetThreadLastViewedAt} from 'selectors/views/threads'; import {ActionTypes} from 'utils/constants'; -import {isThreadOpen, makeGetThreadLastViewedAt} from 'selectors/views/threads'; + +import type {GlobalState} from 'types/store'; export type NewPostMessageProps = { mentions: string[]; @@ -123,10 +119,7 @@ export function setChannelReadAndViewed(dispatch: DispatchFunc, getState: GetSta dispatch(markChannelAsViewedOnServer(post.channel_id)); } - return [ - ...actionsToMarkChannelAsRead(getState, post.channel_id), - ...actionsToMarkChannelAsViewed(getState, post.channel_id), - ]; + return actionsToMarkChannelAsRead(getState, post.channel_id); } return actionsToMarkChannelAsUnread(getState, websocketMessageProps.team_id, post.channel_id, websocketMessageProps.mentions, fetchedChannelMember, post.root_id === '', post?.metadata?.priority?.priority); diff --git a/webapp/channels/src/actions/notification_actions.jsx b/webapp/channels/src/actions/notification_actions.jsx index 81dba52c5f2..2728d7f9e28 100644 --- a/webapp/channels/src/actions/notification_actions.jsx +++ b/webapp/channels/src/actions/notification_actions.jsx @@ -9,22 +9,26 @@ import { getTeammateNameDisplaySetting, isCollapsedThreadsEnabled, } from 'mattermost-redux/selectors/entities/preferences'; +import {getAllUserMentionKeys} from 'mattermost-redux/selectors/entities/search'; import {getCurrentUserId, getCurrentUser, getStatusForUserId, getUser} from 'mattermost-redux/selectors/entities/users'; import {isChannelMuted} from 'mattermost-redux/utils/channel_utils'; import {isSystemMessage, isUserAddedInChannel} from 'mattermost-redux/utils/post_utils'; import {displayUsername} from 'mattermost-redux/utils/user_utils'; -import {isThreadOpen} from 'selectors/views/threads'; import {getChannelURL, getPermalinkURL} from 'selectors/urls'; +import {isThreadOpen} from 'selectors/views/threads'; import {getHistory} from 'utils/browser_history'; -import Constants, {NotificationLevels, UserStatuses} from 'utils/constants'; +import Constants, {NotificationLevels, UserStatuses, IgnoreChannelMentions} from 'utils/constants'; +import {t} from 'utils/i18n'; +import {stripMarkdown, formatWithRenderer} from 'utils/markdown'; +import MentionableRenderer from 'utils/markdown/mentionable_renderer'; import * as NotificationSounds from 'utils/notification_sounds'; import {showNotification} from 'utils/notifications'; +import {cjkrPattern, escapeRegex} from 'utils/text_formatting'; import {isDesktopApp, isMobileApp, isWindowsApp} from 'utils/user_agent'; import * as Utils from 'utils/utils'; -import {t} from 'utils/i18n'; -import {stripMarkdown} from 'utils/markdown'; + import {runDesktopNotificationHooks} from './hooks'; const NOTIFY_TEXT_MAX_LENGTH = 50; @@ -92,14 +96,94 @@ export function sendDesktopNotification(post, msgProps) { return; } - let notifyLevel = member?.notify_props?.desktop || NotificationLevels.DEFAULT; + const channelNotifyProp = member?.notify_props?.desktop || NotificationLevels.DEFAULT; + let notifyLevel = channelNotifyProp; if (notifyLevel === NotificationLevels.DEFAULT) { notifyLevel = user?.notify_props?.desktop || NotificationLevels.ALL; } + if (channel.type === 'G' && channelNotifyProp === NotificationLevels.DEFAULT && user?.notify_props?.desktop === NotificationLevels.MENTION) { + notifyLevel = NotificationLevels.ALL; + } + if (notifyLevel === NotificationLevels.NONE) { return; + } else if (channel.type === 'G' && notifyLevel === NotificationLevels.MENTION) { + // Compose the whole text in the message, including interactive messages. + let text = post.message; + + // We do this on a try catch block to avoid errors from malformed props + try { + if (post.props && post.props.attachments) { + const attachments = post.props.attachments; + function appendText(toAppend) { + if (toAppend) { + text += `\n${toAppend}`; + } + } + for (const attachment of attachments) { + appendText(attachment.pretext); + appendText(attachment.title); + appendText(attachment.text); + appendText(attachment.footer); + if (attachment.fields) { + for (const field of attachment.fields) { + appendText(field.title); + appendText(field.value); + } + } + } + } + } catch (e) { + // eslint-disable-next-line no-console + console.log('Could not process the whole attachment for mentions', e); + } + + const allMentions = getAllUserMentionKeys(state); + + const ignoreChannelMentionProp = member?.notify_props?.ignore_channel_mentions || IgnoreChannelMentions.DEFAULT; + let ignoreChannelMention = ignoreChannelMentionProp === IgnoreChannelMentions.ON; + if (ignoreChannelMentionProp === IgnoreChannelMentions.DEFAULT) { + ignoreChannelMention = user?.notify_props?.channel === 'false'; + } + + const mentionableText = formatWithRenderer(text, new MentionableRenderer()); + let isExplicitlyMentioned = false; + for (const mention of allMentions) { + if (!mention || !mention.key) { + continue; + } + + if (ignoreChannelMention && ['@all', '@here', '@channel'].includes(mention.key)) { + continue; + } + + let flags = 'g'; + if (!mention.caseSensitive) { + flags += 'i'; + } + + let pattern; + if (cjkrPattern.test(mention.key)) { + // In the case of CJK mention key, even if there's no delimiters (such as spaces) at both ends of a word, it is recognized as a mention key + pattern = new RegExp(`()(${escapeRegex(mention.key)})()`, flags); + } else { + pattern = new RegExp( + `(^|\\W)(${escapeRegex(mention.key)})(\\b|_+\\b)`, + flags, + ); + } + + if (pattern.test(mentionableText)) { + isExplicitlyMentioned = true; + break; + } + } + + if (!isExplicitlyMentioned) { + return; + } } else if (notifyLevel === NotificationLevels.MENTION && mentions.indexOf(user.id) === -1 && msgProps.channel_type !== Constants.DM_CHANNEL) { return; } else if (isCrtReply && notifyLevel === NotificationLevels.ALL && followers.indexOf(currentUserId) === -1) { diff --git a/webapp/channels/src/actions/notification_actions.test.js b/webapp/channels/src/actions/notification_actions.test.js index 873a8e6e10f..0ee8e91984f 100644 --- a/webapp/channels/src/actions/notification_actions.test.js +++ b/webapp/channels/src/actions/notification_actions.test.js @@ -2,7 +2,6 @@ // See LICENSE.txt for license information. import testConfigureStore from 'tests/test_store'; - import {getHistory} from 'utils/browser_history'; import Constants, {NotificationLevels, UserStatuses} from 'utils/constants'; import * as NotificationSounds from 'utils/notification_sounds'; @@ -37,6 +36,9 @@ describe('notification_actions', () => { desktop: NotificationLevels.ALL, desktop_sound: false, desktop_threads: NotificationLevels.ALL, + mention_keys: 'mentionkey', + first_name: 'true', + channel: 'true', }; post = { @@ -80,8 +82,13 @@ describe('notification_actions', () => { current_user_id: { id: 'current_user_id', notify_props: userSettings, + username: 'currentusername', + first_name: 'currentuserfirstname', }, }, + profilesInChannel: { + gm_channel: new Set(['current_user_id']), + }, }, teams: { currentTeamId: 'team_id', @@ -110,12 +117,20 @@ describe('notification_actions', () => { id: 'another_channel_id', team_id: 'team_id', }, + gm_channel: { + id: 'gm_channel', + type: 'G', + }, }, myMembers: { channel_id: { id: 'current_user_id', notify_props: channelSettings, }, + gm_channel: { + id: 'gm_channel', + notify_props: channelSettings, + }, }, membersInChannel: { channel_id: { @@ -124,6 +139,12 @@ describe('notification_actions', () => { notify_props: channelSettings, }, }, + gm_channel: { + current_user_id: { + id: 'gm_channel', + notify_props: channelSettings, + }, + }, muted_channel_id: { current_user_id: { id: 'current_user_id', @@ -139,6 +160,10 @@ describe('notification_actions', () => { 'display_settings--collapsed_reply_threads': crt, }, }, + groups: { + groups: {}, + myGroups: [], + }, }, views: { browser: { @@ -402,5 +427,85 @@ describe('notification_actions', () => { }); }); }); + + describe('GMs', () => { + test('should notify for any message when channel setting is DEFAULT and user setting is MENTION', async () => { + const store = testConfigureStore(baseState); + userSettings.desktop = NotificationLevels.MENTION; + channelSettings.desktop = NotificationLevels.DEFAULT; + post.channel_id = 'gm_channel'; + msgProps.team_id = ''; + + return store.dispatch(sendDesktopNotification(post, msgProps)).then(() => { + expect(spy).toHaveBeenCalled(); + }); + }); + test('should not notify for any message when channel setting is DEFAULT and user setting is NONE', async () => { + const store = testConfigureStore(baseState); + userSettings.desktop = NotificationLevels.NONE; + channelSettings.desktop = NotificationLevels.DEFAULT; + post.message = '@username'; + post.channel_id = 'gm_channel'; + msgProps.team_id = ''; + + return store.dispatch(sendDesktopNotification(post, msgProps)).then(() => { + expect(spy).not.toHaveBeenCalled(); + }); + }); + test('should notify when channel setting MENTION and there is a explicit mention', async () => { + const store = testConfigureStore(baseState); + channelSettings.desktop = NotificationLevels.MENTION; + post.message = '@currentusername'; + post.channel_id = 'gm_channel'; + msgProps.team_id = ''; + + return store.dispatch(sendDesktopNotification(post, msgProps)).then(() => { + expect(spy).toHaveBeenCalled(); + }); + }); + test('should notify when channel setting MENTION and there is a keyword mention', async () => { + const store = testConfigureStore(baseState); + channelSettings.desktop = NotificationLevels.MENTION; + post.message = 'mentionkey'; + post.channel_id = 'gm_channel'; + msgProps.team_id = ''; + + return store.dispatch(sendDesktopNotification(post, msgProps)).then(() => { + expect(spy).toHaveBeenCalled(); + }); + }); + test('should notify when channel setting MENTION and there is the first name', async () => { + const store = testConfigureStore(baseState); + channelSettings.desktop = NotificationLevels.MENTION; + post.message = 'currentuserfirstname'; + post.channel_id = 'gm_channel'; + msgProps.team_id = ''; + + return store.dispatch(sendDesktopNotification(post, msgProps)).then(() => { + expect(spy).toHaveBeenCalled(); + }); + }); + test('should notify when channel setting MENTION and there is a channel mention', async () => { + const store = testConfigureStore(baseState); + channelSettings.desktop = NotificationLevels.MENTION; + post.message = '@all'; + post.channel_id = 'gm_channel'; + msgProps.team_id = ''; + + return store.dispatch(sendDesktopNotification(post, msgProps)).then(() => { + expect(spy).toHaveBeenCalled(); + }); + }); + test('should not notify when channel setting MENTION and there is no explicit mention', async () => { + const store = testConfigureStore(baseState); + channelSettings.desktop = NotificationLevels.MENTION; + post.channel_id = 'gm_channel'; + msgProps.team_id = ''; + + return store.dispatch(sendDesktopNotification(post, msgProps)).then(() => { + expect(spy).not.toHaveBeenCalled(); + }); + }); + }); }); }); diff --git a/webapp/channels/src/actions/post_actions.test.ts b/webapp/channels/src/actions/post_actions.test.ts index 7219c4f8186..801da3dad84 100644 --- a/webapp/channels/src/actions/post_actions.test.ts +++ b/webapp/channels/src/actions/post_actions.test.ts @@ -1,18 +1,19 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import {Post} from '@mattermost/types/posts'; -import {FileInfo} from '@mattermost/types/files'; +import type {FileInfo} from '@mattermost/types/files'; +import type {Post} from '@mattermost/types/posts'; -import {GlobalState} from 'types/store'; import {ChannelTypes, SearchTypes} from 'mattermost-redux/action_types'; import * as PostActions from 'mattermost-redux/actions/posts'; import {Posts} from 'mattermost-redux/constants'; import * as Actions from 'actions/post_actions'; -import {Constants, ActionTypes, RHSStates} from 'utils/constants'; import mockStore from 'tests/test_store'; +import {Constants, ActionTypes, RHSStates} from 'utils/constants'; + +import type {GlobalState} from 'types/store'; jest.mock('mattermost-redux/actions/posts', () => ({ addReaction: (...args: any[]) => ({type: 'MOCK_ADD_REACTION', args}), diff --git a/webapp/channels/src/actions/post_actions.ts b/webapp/channels/src/actions/post_actions.ts index ce234d7b0c7..08646bb5788 100644 --- a/webapp/channels/src/actions/post_actions.ts +++ b/webapp/channels/src/actions/post_actions.ts @@ -1,32 +1,32 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import {Post} from '@mattermost/types/posts'; -import {GroupChannel} from '@mattermost/types/groups'; -import {FileInfo} from '@mattermost/types/files'; +import type {FileInfo} from '@mattermost/types/files'; +import type {GroupChannel} from '@mattermost/types/groups'; +import type {Post} from '@mattermost/types/posts'; import {SearchTypes} from 'mattermost-redux/action_types'; import {getMyChannelMember} from 'mattermost-redux/actions/channels'; -import {getChannel, getMyChannelMember as getMyChannelMemberSelector} from 'mattermost-redux/selectors/entities/channels'; -import {isCollapsedThreadsEnabled} from 'mattermost-redux/selectors/entities/preferences'; -import * as ThreadActions from 'mattermost-redux/actions/threads'; import * as PostActions from 'mattermost-redux/actions/posts'; +import * as ThreadActions from 'mattermost-redux/actions/threads'; +import {getChannel, getMyChannelMember as getMyChannelMemberSelector} from 'mattermost-redux/selectors/entities/channels'; import * as PostSelectors from 'mattermost-redux/selectors/entities/posts'; -import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users'; +import {isCollapsedThreadsEnabled} from 'mattermost-redux/selectors/entities/preferences'; import {getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams'; +import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users'; +import type {DispatchFunc, GetStateFunc} from 'mattermost-redux/types/actions'; import {canEditPost, comparePosts} from 'mattermost-redux/utils/post_utils'; -import {DispatchFunc, GetStateFunc} from 'mattermost-redux/types/actions'; import {addRecentEmoji, addRecentEmojis} from 'actions/emoji_actions'; import * as StorageActions from 'actions/storage'; import {loadNewDMIfNeeded, loadNewGMIfNeeded} from 'actions/user_actions'; +import {removeDraft} from 'actions/views/drafts'; import * as RhsActions from 'actions/views/rhs'; import {manuallyMarkThreadAsUnread} from 'actions/views/threads'; -import {removeDraft} from 'actions/views/drafts'; import {isEmbedVisible, isInlineImageVisible} from 'selectors/posts'; import {getSelectedPostId, getSelectedPostCardId, getRhsState} from 'selectors/rhs'; import {getGlobalItem} from 'selectors/storage'; -import {GlobalState} from 'types/store'; + import { ActionTypes, Constants, @@ -36,7 +36,10 @@ import { import {matchEmoticons} from 'utils/emoticons'; import * as UserAgent from 'utils/user_agent'; -import {completePostReceive, NewPostMessageProps} from './new_post'; +import type {GlobalState} from 'types/store'; + +import {completePostReceive} from './new_post'; +import type {NewPostMessageProps} from './new_post'; export function handleNewPost(post: Post, msg?: {data?: NewPostMessageProps & GroupChannel}) { return async (dispatch: DispatchFunc, getState: GetStateFunc) => { diff --git a/webapp/channels/src/actions/status_actions.test.ts b/webapp/channels/src/actions/status_actions.test.ts index dc78ab16e06..863b4b80c5f 100644 --- a/webapp/channels/src/actions/status_actions.test.ts +++ b/webapp/channels/src/actions/status_actions.test.ts @@ -3,16 +3,17 @@ import {cloneDeep} from 'lodash'; -import {UserProfile} from '@mattermost/types/users'; +import type {UserProfile} from '@mattermost/types/users'; -import {Preferences} from 'mattermost-redux/constants'; import {getStatusesByIds} from 'mattermost-redux/actions/users'; +import {Preferences} from 'mattermost-redux/constants'; import * as Actions from 'actions/status_actions'; -import {GlobalState} from 'types/store'; import mockStore from 'tests/test_store'; +import type {GlobalState} from 'types/store'; + jest.mock('mattermost-redux/actions/users', () => ({ getStatusesByIds: jest.fn(() => { return {type: ''}; diff --git a/webapp/channels/src/actions/status_actions.ts b/webapp/channels/src/actions/status_actions.ts index 6bbc55f1518..4b35ee0035d 100644 --- a/webapp/channels/src/actions/status_actions.ts +++ b/webapp/channels/src/actions/status_actions.ts @@ -1,19 +1,21 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import {UserProfile} from '@mattermost/types/users'; +import type {UserProfile} from '@mattermost/types/users'; import {getStatusesByIds} from 'mattermost-redux/actions/users'; import {getCurrentChannelId} from 'mattermost-redux/selectors/entities/channels'; import {getPostsInCurrentChannel} from 'mattermost-redux/selectors/entities/posts'; import {getDirectShowPreferences} from 'mattermost-redux/selectors/entities/preferences'; import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users'; -import {ActionFunc, DispatchFunc, GetStateFunc} from 'mattermost-redux/types/actions'; -import {GlobalState} from 'types/store'; +import type {ActionFunc, DispatchFunc, GetStateFunc} from 'mattermost-redux/types/actions'; import {loadCustomEmojisForCustomStatusesByUserIds} from 'actions/emoji_actions'; + import {Constants} from 'utils/constants'; +import type {GlobalState} from 'types/store'; + export function loadStatusesForChannelAndSidebar(): ActionFunc { return (dispatch: DispatchFunc, getState: GetStateFunc) => { const state = getState(); diff --git a/webapp/channels/src/actions/storage.ts b/webapp/channels/src/actions/storage.ts index c0b7e2a8785..928664233bf 100644 --- a/webapp/channels/src/actions/storage.ts +++ b/webapp/channels/src/actions/storage.ts @@ -1,7 +1,7 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import {DispatchFunc, GetStateFunc} from 'mattermost-redux/types/actions'; +import type {DispatchFunc, GetStateFunc} from 'mattermost-redux/types/actions'; import {StorageTypes} from 'utils/constants'; import {getPrefix} from 'utils/storage_utils'; diff --git a/webapp/channels/src/actions/team_actions.test.ts b/webapp/channels/src/actions/team_actions.test.ts index 55d8078731d..cfcede5f652 100644 --- a/webapp/channels/src/actions/team_actions.test.ts +++ b/webapp/channels/src/actions/team_actions.test.ts @@ -1,14 +1,13 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import * as TeamActions from 'mattermost-redux/actions/teams'; import * as channelActions from 'mattermost-redux/actions/channels'; +import * as TeamActions from 'mattermost-redux/actions/teams'; import * as userActions from 'mattermost-redux/actions/users'; import * as Actions from 'actions/team_actions'; import configureStore from 'tests/test_store'; - import {getHistory} from 'utils/browser_history'; import {TestHelper} from 'utils/test_helper'; diff --git a/webapp/channels/src/actions/team_actions.ts b/webapp/channels/src/actions/team_actions.ts index 90134b137b7..414c883578d 100644 --- a/webapp/channels/src/actions/team_actions.ts +++ b/webapp/channels/src/actions/team_actions.ts @@ -1,19 +1,21 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import {Team} from '@mattermost/types/teams'; -import {ServerError} from '@mattermost/types/errors'; -import {UserProfile} from '@mattermost/types/users'; +import type {ServerError} from '@mattermost/types/errors'; +import type {Team} from '@mattermost/types/teams'; +import type {UserProfile} from '@mattermost/types/users'; import {TeamTypes} from 'mattermost-redux/action_types'; -import {ActionFunc, DispatchFunc, GetStateFunc} from 'mattermost-redux/types/actions'; import {getChannelStats} from 'mattermost-redux/actions/channels'; -import * as TeamActions from 'mattermost-redux/actions/teams'; -import {getUser} from 'mattermost-redux/actions/users'; +import {logError} from 'mattermost-redux/actions/errors'; import {savePreferences} from 'mattermost-redux/actions/preferences'; +import * as TeamActions from 'mattermost-redux/actions/teams'; import {selectTeam} from 'mattermost-redux/actions/teams'; -import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users'; +import {getUser} from 'mattermost-redux/actions/users'; +import {Client4} from 'mattermost-redux/client'; import {getCurrentChannelId} from 'mattermost-redux/selectors/entities/channels'; +import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users'; +import type {ActionFunc, DispatchFunc, GetStateFunc} from 'mattermost-redux/types/actions'; import {getHistory} from 'utils/browser_history'; import {Preferences} from 'utils/constants'; @@ -114,3 +116,19 @@ export function updateTeamsOrderForUser(teamIds: Array) { dispatch(savePreferences(currentUserId, teamOrderPreferences)); }; } + +export function getGroupMessageMembersCommonTeams(channelId: string): ActionFunc { + return async (dispatch) => { + let teams: Team[]; + + try { + const response = await Client4.getGroupMessageMembersCommonTeams(channelId); + teams = response.data; + } catch (error) { + dispatch(logError(error as ServerError)); + return {error: error as ServerError}; + } + + return {data: teams}; + }; +} diff --git a/webapp/channels/src/actions/telemetry_actions.jsx b/webapp/channels/src/actions/telemetry_actions.jsx index bd2dbb79acc..665e29e7aac 100644 --- a/webapp/channels/src/actions/telemetry_actions.jsx +++ b/webapp/channels/src/actions/telemetry_actions.jsx @@ -8,7 +8,6 @@ import {getConfig, isPerformanceDebuggingEnabled} from 'mattermost-redux/selecto import {getBool} from 'mattermost-redux/selectors/entities/preferences'; import {isDevModeEnabled} from 'selectors/general'; - import store from 'stores/redux_store.jsx'; const SUPPORTS_CLEAR_MARKS = isSupported([performance.clearMarks]); diff --git a/webapp/channels/src/actions/user_actions.test.ts b/webapp/channels/src/actions/user_actions.test.ts index 928253c627b..cf7e2579122 100644 --- a/webapp/channels/src/actions/user_actions.test.ts +++ b/webapp/channels/src/actions/user_actions.test.ts @@ -1,10 +1,10 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import {Channel, ChannelMembership, ChannelMessageCount} from '@mattermost/types/channels'; -import {Team, TeamMembership} from '@mattermost/types/teams'; -import {Post} from '@mattermost/types/posts'; -import {UserProfile} from '@mattermost/types/users'; +import type {Channel, ChannelMembership, ChannelMessageCount} from '@mattermost/types/channels'; +import type {Post} from '@mattermost/types/posts'; +import type {Team, TeamMembership} from '@mattermost/types/teams'; +import type {UserProfile} from '@mattermost/types/users'; import {Preferences, General} from 'mattermost-redux/constants'; import {CategoryTypes} from 'mattermost-redux/constants/channel_categories'; @@ -12,12 +12,12 @@ import {getPreferenceKey} from 'mattermost-redux/utils/preference_utils'; import * as UserActions from 'actions/user_actions'; import store from 'stores/redux_store'; + import TestHelper from 'packages/mattermost-redux/test/test_helper'; - -import {GlobalState} from 'types/store'; - import mockStore from 'tests/test_store'; +import type {GlobalState} from 'types/store'; + jest.mock('mattermost-redux/actions/users', () => { const original = jest.requireActual('mattermost-redux/actions/users'); return { diff --git a/webapp/channels/src/actions/user_actions.ts b/webapp/channels/src/actions/user_actions.ts index 0950969b0ae..1e6b7b682e9 100644 --- a/webapp/channels/src/actions/user_actions.ts +++ b/webapp/channels/src/actions/user_actions.ts @@ -3,10 +3,8 @@ import PQueue from 'p-queue'; -import {UserProfile, UserStatus} from '@mattermost/types/users'; - -import {Channel} from '@mattermost/types/channels'; -import {GlobalState} from 'types/store'; +import type {Channel} from '@mattermost/types/channels'; +import type {UserProfile, UserStatus} from '@mattermost/types/users'; import {getChannelAndMyMember, getChannelMembersByIds} from 'mattermost-redux/actions/channels'; import {savePreferences} from 'mattermost-redux/actions/preferences'; @@ -24,19 +22,18 @@ import { import {getBool, isCollapsedThreadsEnabled} from 'mattermost-redux/selectors/entities/preferences'; import {getCurrentTeamId, getTeamMember} from 'mattermost-redux/selectors/entities/teams'; import * as Selectors from 'mattermost-redux/selectors/entities/users'; -import {ActionResult, DispatchFunc, GetStateFunc} from 'mattermost-redux/types/actions'; - +import type {ActionResult, DispatchFunc, GetStateFunc} from 'mattermost-redux/types/actions'; import {calculateUnreadCount} from 'mattermost-redux/utils/channel_utils'; import {loadCustomEmojisForCustomStatusesByUserIds} from 'actions/emoji_actions'; import {loadStatusesForProfilesList, loadStatusesForProfilesMap} from 'actions/status_actions'; - import {getDisplayedChannels} from 'selectors/views/channel_sidebar'; - import store from 'stores/redux_store.jsx'; -import * as Utils from 'utils/utils'; import {Constants, Preferences, UserStatuses} from 'utils/constants'; +import * as Utils from 'utils/utils'; + +import type {GlobalState} from 'types/store'; export const queue = new PQueue({concurrency: 4}); const dispatch = store.dispatch; diff --git a/webapp/channels/src/actions/views/browser.ts b/webapp/channels/src/actions/views/browser.ts index 50a949d1f33..afb3e8023f5 100644 --- a/webapp/channels/src/actions/views/browser.ts +++ b/webapp/channels/src/actions/views/browser.ts @@ -1,7 +1,7 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import {GenericAction} from 'mattermost-redux/types/actions'; +import type {GenericAction} from 'mattermost-redux/types/actions'; import {Constants, ActionTypes, WindowSizes} from 'utils/constants'; diff --git a/webapp/channels/src/actions/views/channel.test.js b/webapp/channels/src/actions/views/channel.test.js index 872a1c2d5b1..ab17d8444b2 100644 --- a/webapp/channels/src/actions/views/channel.test.js +++ b/webapp/channels/src/actions/views/channel.test.js @@ -1,15 +1,16 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import {General, Posts, RequestStatus} from 'mattermost-redux/constants'; import {leaveChannel, markChannelAsRead, getChannel} from 'mattermost-redux/actions/channels'; -import * as UserActions from 'mattermost-redux/actions/users'; import * as PostActions from 'mattermost-redux/actions/posts'; +import * as UserActions from 'mattermost-redux/actions/users'; +import {General, Posts, RequestStatus} from 'mattermost-redux/constants'; -import {getHistory} from 'utils/browser_history'; import * as Actions from 'actions/views/channel'; import {closeRightHandSide} from 'actions/views/rhs'; + import mockStore from 'tests/test_store'; +import {getHistory} from 'utils/browser_history'; import {ActionTypes, PostRequestTypes} from 'utils/constants'; jest.mock('utils/channel_utils.tsx', () => { diff --git a/webapp/channels/src/actions/views/channel.ts b/webapp/channels/src/actions/views/channel.ts index 96eb19045dd..f0783eda35e 100644 --- a/webapp/channels/src/actions/views/channel.ts +++ b/webapp/channels/src/actions/views/channel.ts @@ -1,11 +1,12 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. +import type {AnyAction} from 'redux'; import {batchActions} from 'redux-batched-actions'; -import {AnyAction} from 'redux'; -import {Channel} from '@mattermost/types/channels'; +import type {Channel} from '@mattermost/types/channels'; +import {TeamTypes} from 'mattermost-redux/action_types'; import { leaveChannel as leaveChannelRedux, joinChannel, @@ -15,12 +16,9 @@ import { getChannel as loadChannel, } from 'mattermost-redux/actions/channels'; import * as PostActions from 'mattermost-redux/actions/posts'; -import {TeamTypes} from 'mattermost-redux/action_types'; -import {DispatchFunc, GetStateFunc} from 'mattermost-redux/types/actions'; -import {autocompleteUsers} from 'mattermost-redux/actions/users'; import {selectTeam} from 'mattermost-redux/actions/teams'; +import {autocompleteUsers} from 'mattermost-redux/actions/users'; import {Posts, RequestStatus} from 'mattermost-redux/constants'; - import { getChannel, getChannelsNameMapInCurrentTeam, @@ -33,6 +31,7 @@ import { isManuallyUnread, getCurrentChannelId, } from 'mattermost-redux/selectors/entities/channels'; +import {getMostRecentPostIdInChannel, getPost} from 'mattermost-redux/selectors/entities/posts'; import { getCurrentRelativeTeamUrl, getCurrentTeam, @@ -41,36 +40,25 @@ import { getTeamsList, } from 'mattermost-redux/selectors/entities/teams'; import {getCurrentUserId, getUserByUsername} from 'mattermost-redux/selectors/entities/users'; -import {getMostRecentPostIdInChannel, getPost} from 'mattermost-redux/selectors/entities/posts'; import {makeAddLastViewAtToProfiles} from 'mattermost-redux/selectors/entities/utils'; - +import type {DispatchFunc, GetStateFunc} from 'mattermost-redux/types/actions'; import {getChannelByName} from 'mattermost-redux/utils/channel_utils'; import EventEmitter from 'mattermost-redux/utils/event_emitter'; -import {closeRightHandSide} from 'actions/views/rhs'; import {openDirectChannelToUserId} from 'actions/channel_actions'; import {loadCustomStatusEmojisForPostList} from 'actions/emoji_actions'; +import {closeRightHandSide} from 'actions/views/rhs'; import {getLastViewedChannelName} from 'selectors/local_storage'; +import {getSelectedPost, getSelectedPostId} from 'selectors/rhs'; import {getLastPostsApiTimeForChannel} from 'selectors/views/channel'; import {getSocketStatus} from 'selectors/views/websocket'; -import {getSelectedPost, getSelectedPostId} from 'selectors/rhs'; +import LocalStorageStore from 'stores/local_storage_store'; import {getHistory} from 'utils/browser_history'; -import {Constants, ActionTypes, EventTypes, PostRequestTypes} from 'utils/constants'; -import {isMobile} from 'utils/utils'; -import LocalStorageStore from 'stores/local_storage_store'; import {isArchivedChannel} from 'utils/channel_utils'; -import type {GlobalState} from 'types/store'; +import {Constants, ActionTypes, EventTypes, PostRequestTypes} from 'utils/constants'; -export function checkAndSetMobileView() { - return (dispatch: DispatchFunc) => { - dispatch({ - type: ActionTypes.UPDATE_MOBILE_VIEW, - data: isMobile(), - }); - return {data: true}; - }; -} +import type {GlobalState} from 'types/store'; export function goToLastViewedChannel() { return async (dispatch: DispatchFunc, getState: GetStateFunc) => { diff --git a/webapp/channels/src/actions/views/channel_sidebar.test.ts b/webapp/channels/src/actions/views/channel_sidebar.test.ts index de4c60e4e9c..97e357b6203 100644 --- a/webapp/channels/src/actions/views/channel_sidebar.test.ts +++ b/webapp/channels/src/actions/views/channel_sidebar.test.ts @@ -2,7 +2,7 @@ // See LICENSE.txt for license information. import {CategorySorting} from '@mattermost/types/channel_categories'; -import {Channel, ChannelMembership} from '@mattermost/types/channels'; +import type {Channel, ChannelMembership} from '@mattermost/types/channels'; import {insertWithoutDuplicates} from 'mattermost-redux/utils/array_utils'; diff --git a/webapp/channels/src/actions/views/channel_sidebar.ts b/webapp/channels/src/actions/views/channel_sidebar.ts index 11f9700dac4..b62a6f62b9c 100644 --- a/webapp/channels/src/actions/views/channel_sidebar.ts +++ b/webapp/channels/src/actions/views/channel_sidebar.ts @@ -6,13 +6,15 @@ import {General} from 'mattermost-redux/constants'; import {CategoryTypes} from 'mattermost-redux/constants/channel_categories'; import {getCategory, makeGetChannelIdsForCategory} from 'mattermost-redux/selectors/entities/channel_categories'; import {getCurrentChannelId} from 'mattermost-redux/selectors/entities/channels'; -import {DispatchFunc, GetStateFunc} from 'mattermost-redux/types/actions'; +import type {DispatchFunc, GetStateFunc} from 'mattermost-redux/types/actions'; import {insertMultipleWithoutDuplicates} from 'mattermost-redux/utils/array_utils'; import {getCategoriesForCurrentTeam, getChannelsInCategoryOrder, getDisplayedChannels} from 'selectors/views/channel_sidebar'; -import {DraggingState, GlobalState} from 'types/store'; + import {ActionTypes} from 'utils/constants'; +import type {DraggingState, GlobalState} from 'types/store'; + export function setUnreadFilterEnabled(enabled: boolean) { return { type: ActionTypes.SET_UNREAD_FILTER_ENABLED, diff --git a/webapp/channels/src/actions/views/create_comment.test.jsx b/webapp/channels/src/actions/views/create_comment.test.jsx index 1f4ee9cc433..4d18770cc46 100644 --- a/webapp/channels/src/actions/views/create_comment.test.jsx +++ b/webapp/channels/src/actions/views/create_comment.test.jsx @@ -8,6 +8,10 @@ import { } from 'mattermost-redux/actions/posts'; import {Posts} from 'mattermost-redux/constants'; +import {executeCommand} from 'actions/command'; +import * as HookActions from 'actions/hooks'; +import * as PostActions from 'actions/post_actions'; +import {setGlobalItem, actionOnGlobalItemsWithPrefix} from 'actions/storage'; import { clearCommentDraftUploads, updateCommentDraft, @@ -19,13 +23,9 @@ import { makeOnEditLatestPost, } from 'actions/views/create_comment'; import {removeDraft, setGlobalDraftSource} from 'actions/views/drafts'; -import {setGlobalItem, actionOnGlobalItemsWithPrefix} from 'actions/storage'; -import * as PostActions from 'actions/post_actions'; -import {executeCommand} from 'actions/command'; -import * as HookActions from 'actions/hooks'; -import {StoragePrefixes} from 'utils/constants'; import mockStore from 'tests/test_store'; +import {StoragePrefixes} from 'utils/constants'; /* eslint-disable global-require */ diff --git a/webapp/channels/src/actions/views/create_comment.tsx b/webapp/channels/src/actions/views/create_comment.tsx index 39037b215bb..3fd7fe681f3 100644 --- a/webapp/channels/src/actions/views/create_comment.tsx +++ b/webapp/channels/src/actions/views/create_comment.tsx @@ -1,17 +1,8 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import {Post} from '@mattermost/types/posts'; +import type {Post} from '@mattermost/types/posts'; -import {createSelector} from 'mattermost-redux/selectors/create_selector'; -import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users'; -import {getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams'; -import { - makeGetMessageInHistoryItem, - getPost, - makeGetPostIdsForThread, -} from 'mattermost-redux/selectors/entities/posts'; -import {getCustomEmojisByName} from 'mattermost-redux/selectors/entities/emojis'; import { removeReaction, addMessageIntoHistory, @@ -19,21 +10,31 @@ import { moveHistoryIndexForward, } from 'mattermost-redux/actions/posts'; import {Posts} from 'mattermost-redux/constants'; +import {createSelector} from 'mattermost-redux/selectors/create_selector'; +import {getCustomEmojisByName} from 'mattermost-redux/selectors/entities/emojis'; +import { + makeGetMessageInHistoryItem, + getPost, + makeGetPostIdsForThread, +} from 'mattermost-redux/selectors/entities/posts'; +import {getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams'; +import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users'; +import type {DispatchFunc, GetStateFunc} from 'mattermost-redux/types/actions'; import {isPostPendingOrFailed} from 'mattermost-redux/utils/post_utils'; -import * as PostActions from 'actions/post_actions'; import {executeCommand} from 'actions/command'; import {runMessageWillBePostedHooks, runSlashCommandWillBePostedHooks} from 'actions/hooks'; +import * as PostActions from 'actions/post_actions'; import {actionOnGlobalItemsWithPrefix} from 'actions/storage'; import {updateDraft, removeDraft} from 'actions/views/drafts'; -import EmojiMap from 'utils/emoji_map'; import {getPostDraft} from 'selectors/rhs'; -import * as Utils from 'utils/utils'; import {Constants, StoragePrefixes} from 'utils/constants'; -import type {PostDraft} from 'types/store/draft'; +import EmojiMap from 'utils/emoji_map'; +import * as Utils from 'utils/utils'; + import type {GlobalState} from 'types/store'; -import type {DispatchFunc, GetStateFunc} from 'mattermost-redux/types/actions'; +import type {PostDraft} from 'types/store/draft'; export function clearCommentDraftUploads() { return actionOnGlobalItemsWithPrefix(StoragePrefixes.COMMENT_DRAFT, (_key: string, draft: PostDraft) => { diff --git a/webapp/channels/src/actions/views/drafts.test.ts b/webapp/channels/src/actions/views/drafts.test.ts index eee7e8ccfb2..94b1b4f34bf 100644 --- a/webapp/channels/src/actions/views/drafts.test.ts +++ b/webapp/channels/src/actions/views/drafts.test.ts @@ -1,17 +1,16 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import {setGlobalItem} from 'actions/storage'; -import {PostDraft} from 'types/store/draft'; -import {StoragePrefixes} from 'utils/constants'; - -import mockStore from 'tests/test_store'; - +import {Client4} from 'mattermost-redux/client'; import {Posts, Preferences} from 'mattermost-redux/constants'; - import {getPreferenceKey} from 'mattermost-redux/utils/preference_utils'; -import {Client4} from 'mattermost-redux/client'; +import {setGlobalItem} from 'actions/storage'; + +import mockStore from 'tests/test_store'; +import {StoragePrefixes} from 'utils/constants'; + +import type {PostDraft} from 'types/store/draft'; import {removeDraft, setGlobalDraftSource, updateDraft} from './drafts'; diff --git a/webapp/channels/src/actions/views/drafts.ts b/webapp/channels/src/actions/views/drafts.ts index 7d997f9fd69..67947663b76 100644 --- a/webapp/channels/src/actions/views/drafts.ts +++ b/webapp/channels/src/actions/views/drafts.ts @@ -3,27 +3,28 @@ import {batchActions} from 'redux-batched-actions'; -import {ActionFunc, DispatchFunc, GetStateFunc} from 'mattermost-redux/types/actions'; -import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users'; -import {syncedDraftsAreAllowedAndEnabled} from 'mattermost-redux/selectors/entities/preferences'; +import type {Draft as ServerDraft} from '@mattermost/types/drafts'; +import type {FileInfo} from '@mattermost/types/files'; +import type {PostMetadata, PostPriorityMetadata} from '@mattermost/types/posts'; +import type {PreferenceType} from '@mattermost/types/preferences'; +import type {UserProfile} from '@mattermost/types/users'; + +import {savePreferences} from 'mattermost-redux/actions/preferences'; import {Client4} from 'mattermost-redux/client'; +import Preferences from 'mattermost-redux/constants/preferences'; +import {syncedDraftsAreAllowedAndEnabled} from 'mattermost-redux/selectors/entities/preferences'; +import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users'; +import type {ActionFunc, DispatchFunc, GetStateFunc} from 'mattermost-redux/types/actions'; import {setGlobalItem} from 'actions/storage'; -import {getConnectionId} from 'selectors/general'; -import type {GlobalState} from 'types/store'; -import {PostDraft} from 'types/store/draft'; -import {getGlobalItem} from 'selectors/storage'; import {makeGetDrafts} from 'selectors/drafts'; +import {getConnectionId} from 'selectors/general'; +import {getGlobalItem} from 'selectors/storage'; import {ActionTypes, StoragePrefixes} from 'utils/constants'; -import type {Draft as ServerDraft} from '@mattermost/types/drafts'; -import type {UserProfile} from '@mattermost/types/users'; -import {PostMetadata, PostPriorityMetadata} from '@mattermost/types/posts'; -import {FileInfo} from '@mattermost/types/files'; -import {PreferenceType} from '@mattermost/types/preferences'; -import {savePreferences} from 'mattermost-redux/actions/preferences'; -import Preferences from 'mattermost-redux/constants/preferences'; +import type {GlobalState} from 'types/store'; +import type {PostDraft} from 'types/store/draft'; type Draft = { key: keyof GlobalState['storage']['storage']; diff --git a/webapp/channels/src/actions/views/group.js b/webapp/channels/src/actions/views/group.js index 1819ef54af9..951093277a5 100644 --- a/webapp/channels/src/actions/views/group.js +++ b/webapp/channels/src/actions/views/group.js @@ -1,14 +1,14 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import {searchAssociatedGroupsForReferenceLocal} from 'mattermost-redux/selectors/entities/groups'; -import {getConfig} from 'mattermost-redux/selectors/entities/general'; -import {isCustomGroupsEnabled} from 'mattermost-redux/selectors/entities/preferences'; -import {haveIChannelPermission} from 'mattermost-redux/selectors/entities/roles'; import {searchGroups} from 'mattermost-redux/actions/groups'; import Permissions from 'mattermost-redux/constants/permissions'; +import {getConfig} from 'mattermost-redux/selectors/entities/general'; +import {searchAssociatedGroupsForReferenceLocal} from 'mattermost-redux/selectors/entities/groups'; +import {isCustomGroupsEnabled} from 'mattermost-redux/selectors/entities/preferences'; +import {haveIChannelPermission} from 'mattermost-redux/selectors/entities/roles'; -export function searchAssociatedGroupsForReference(prefix, teamId, channelId) { +export function searchAssociatedGroupsForReference(prefix, teamId, channelId, opts = {}) { return async (dispatch, getState) => { const state = getState(); if (!haveIChannelPermission(state, @@ -23,7 +23,7 @@ export function searchAssociatedGroupsForReference(prefix, teamId, channelId) { const isTimezoneEnabled = config.ExperimentalTimezone === 'true'; if (isCustomGroupsEnabled(state)) { - await dispatch(searchGroups({ + const params = { q: prefix, filter_allow_reference: true, page: 0, @@ -31,7 +31,10 @@ export function searchAssociatedGroupsForReference(prefix, teamId, channelId) { include_member_count: true, include_channel_member_count: channelId, include_timezones: isTimezoneEnabled, - })); + ...opts, + }; + + await dispatch(searchGroups(params)); } return {data: searchAssociatedGroupsForReferenceLocal(state, prefix, teamId, channelId)}; }; diff --git a/webapp/channels/src/actions/views/lhs.test.ts b/webapp/channels/src/actions/views/lhs.test.ts index f8d0425ada4..e50be56bc50 100644 --- a/webapp/channels/src/actions/views/lhs.test.ts +++ b/webapp/channels/src/actions/views/lhs.test.ts @@ -1,15 +1,17 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import {MockStoreEnhanced} from 'redux-mock-store'; +import type {MockStoreEnhanced} from 'redux-mock-store'; -import {DispatchFunc} from 'mattermost-redux/types/actions'; +import type {DispatchFunc} from 'mattermost-redux/types/actions'; import {close, open, toggle} from 'actions/views/lhs'; -import {ActionTypes} from 'utils/constants'; -import mockStore from 'tests/test_store'; import configureStore from 'store'; -import {GlobalState} from 'types/store'; + +import mockStore from 'tests/test_store'; +import {ActionTypes} from 'utils/constants'; + +import type {GlobalState} from 'types/store'; import * as Actions from './lhs'; diff --git a/webapp/channels/src/actions/views/lhs.ts b/webapp/channels/src/actions/views/lhs.ts index 8b9927699df..42e5d2f52dd 100644 --- a/webapp/channels/src/actions/views/lhs.ts +++ b/webapp/channels/src/actions/views/lhs.ts @@ -2,13 +2,45 @@ // See LICENSE.txt for license information. import {selectChannel} from 'mattermost-redux/actions/channels'; -import {DispatchFunc, GetStateFunc} from 'mattermost-redux/types/actions'; import {getCurrentRelativeTeamUrl} from 'mattermost-redux/selectors/entities/teams'; +import type {DispatchFunc, GetStateFunc} from 'mattermost-redux/types/actions'; + +import {SidebarSize} from 'components/resizable_sidebar/constants'; -import {GlobalState} from 'types/store'; -import {LhsItemType} from 'types/store/lhs'; -import {ActionTypes} from 'utils/constants'; import {getHistory} from 'utils/browser_history'; +import Constants, {ActionTypes} from 'utils/constants'; + +import type {GlobalState} from 'types/store'; +import {LhsItemType} from 'types/store/lhs'; + +export const setLhsSize = (sidebarSize?: SidebarSize) => { + let newSidebarSize = sidebarSize; + if (!sidebarSize) { + const width = window.innerWidth; + + switch (true) { + case width <= Constants.SMALL_SIDEBAR_BREAKPOINT: { + newSidebarSize = SidebarSize.SMALL; + break; + } + case width > Constants.SMALL_SIDEBAR_BREAKPOINT && width <= Constants.MEDIUM_SIDEBAR_BREAKPOINT: { + newSidebarSize = SidebarSize.MEDIUM; + break; + } + case width > Constants.MEDIUM_SIDEBAR_BREAKPOINT && width <= Constants.LARGE_SIDEBAR_BREAKPOINT: { + newSidebarSize = SidebarSize.LARGE; + break; + } + default: { + newSidebarSize = SidebarSize.XLARGE; + } + } + } + return { + type: ActionTypes.SET_LHS_SIZE, + size: newSidebarSize, + }; +}; export const toggle = () => ({ type: ActionTypes.TOGGLE_LHS, diff --git a/webapp/channels/src/actions/views/login.test.ts b/webapp/channels/src/actions/views/login.test.ts index 8eed9d48196..da2ea949f90 100644 --- a/webapp/channels/src/actions/views/login.test.ts +++ b/webapp/channels/src/actions/views/login.test.ts @@ -5,14 +5,14 @@ import nock from 'nock'; import {Client4} from 'mattermost-redux/client'; -import TestHelper from 'packages/mattermost-redux/test/test_helper'; - import { login, loginById, } from 'actions/views/login'; import configureStore from 'store'; +import TestHelper from 'packages/mattermost-redux/test/test_helper'; + describe('actions/views/login', () => { describe('login', () => { test('should return successful when login is successful', async () => { diff --git a/webapp/channels/src/actions/views/login.ts b/webapp/channels/src/actions/views/login.ts index 524b8dddd8b..6330d6c6a6c 100644 --- a/webapp/channels/src/actions/views/login.ts +++ b/webapp/channels/src/actions/views/login.ts @@ -3,13 +3,13 @@ import {batchActions} from 'redux-batched-actions'; -import {ServerError} from '@mattermost/types/errors'; +import type {ServerError} from '@mattermost/types/errors'; -import {Client4} from 'mattermost-redux/client'; -import {ActionFunc, DispatchFunc} from 'mattermost-redux/types/actions'; import {UserTypes} from 'mattermost-redux/action_types'; import {logError} from 'mattermost-redux/actions/errors'; import {loadRolesIfNeeded} from 'mattermost-redux/actions/roles'; +import {Client4} from 'mattermost-redux/client'; +import type {ActionFunc, DispatchFunc} from 'mattermost-redux/types/actions'; export function login(loginId: string, password: string, mfaToken = ''): ActionFunc { return async (dispatch: DispatchFunc) => { @@ -45,6 +45,40 @@ export function login(loginId: string, password: string, mfaToken = ''): ActionF }; } +export function loginWithDesktopToken(token: string): ActionFunc { + return async (dispatch: DispatchFunc) => { + dispatch({type: UserTypes.LOGIN_REQUEST, data: null}); + + try { + // This is partial user profile we recieved when we login. We still need to make getMe for complete user profile. + const loggedInUserProfile = await Client4.loginWithDesktopToken(token); + + dispatch( + batchActions([ + { + type: UserTypes.LOGIN_SUCCESS, + }, + { + type: UserTypes.RECEIVED_ME, + data: loggedInUserProfile, + }, + ]), + ); + + dispatch(loadRolesIfNeeded(loggedInUserProfile.roles.split(' '))); + } catch (error) { + dispatch({ + type: UserTypes.LOGIN_FAILURE, + error, + }); + dispatch(logError(error as ServerError)); + return {error}; + } + + return {data: true}; + }; +} + export function loginById(id: string, password: string): ActionFunc { return async (dispatch: DispatchFunc) => { dispatch({type: UserTypes.LOGIN_REQUEST, data: null}); diff --git a/webapp/channels/src/actions/views/mfa.test.js b/webapp/channels/src/actions/views/mfa.test.js index 350723e76df..85d29844c55 100644 --- a/webapp/channels/src/actions/views/mfa.test.js +++ b/webapp/channels/src/actions/views/mfa.test.js @@ -10,6 +10,7 @@ import { deactivateMfa, generateMfaSecret, } from 'actions/views/mfa'; + import configureStore from 'tests/test_store'; describe('actions/views/mfa', () => { diff --git a/webapp/channels/src/actions/views/modals.test.jsx b/webapp/channels/src/actions/views/modals.test.jsx index e4b89159e9b..82207d3f3ca 100644 --- a/webapp/channels/src/actions/views/modals.test.jsx +++ b/webapp/channels/src/actions/views/modals.test.jsx @@ -5,9 +5,10 @@ import React from 'react'; import {Modal} from 'react-bootstrap'; import {openModal, closeModal} from 'actions/views/modals'; -import {ActionTypes, ModalIdentifiers} from 'utils/constants'; import mockStore from 'tests/test_store'; +import {ActionTypes, ModalIdentifiers} from 'utils/constants'; + class TestModal extends React.PureComponent { render() { return ( diff --git a/webapp/channels/src/actions/views/modals.ts b/webapp/channels/src/actions/views/modals.ts index 91ef17536c0..f2fd2bdde98 100644 --- a/webapp/channels/src/actions/views/modals.ts +++ b/webapp/channels/src/actions/views/modals.ts @@ -1,10 +1,10 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import {ModalData} from 'types/actions'; - import {ActionTypes} from 'utils/constants'; +import type {ModalData} from 'types/actions'; + export function openModal

(modalData: ModalData

) { return { type: ActionTypes.MODAL_OPEN, diff --git a/webapp/channels/src/actions/views/onboarding_tasks.ts b/webapp/channels/src/actions/views/onboarding_tasks.ts index 4120dc158b6..e9eabd74b7a 100644 --- a/webapp/channels/src/actions/views/onboarding_tasks.ts +++ b/webapp/channels/src/actions/views/onboarding_tasks.ts @@ -1,16 +1,19 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import {getCurrentTeamId, getTeam} from 'mattermost-redux/selectors/entities/teams'; import {getCurrentUser, getCurrentUserId} from 'mattermost-redux/selectors/entities/common'; -import {DispatchFunc, GetStateFunc} from 'mattermost-redux/types/actions'; -import {GlobalState} from 'types/store'; -import {getHistory} from 'utils/browser_history'; -import InvitationModal from 'components/invitation_modal'; -import LocalStorageStore from 'stores/local_storage_store'; -import {ActionTypes, Constants, ModalIdentifiers} from 'utils/constants'; +import {getCurrentTeamId, getTeam} from 'mattermost-redux/selectors/entities/teams'; +import type {DispatchFunc, GetStateFunc} from 'mattermost-redux/types/actions'; import {getTeamRedirectChannelIfIsAccesible} from 'actions/global_actions'; +import LocalStorageStore from 'stores/local_storage_store'; + +import InvitationModal from 'components/invitation_modal'; + +import {getHistory} from 'utils/browser_history'; +import {ActionTypes, Constants, ModalIdentifiers} from 'utils/constants'; + +import type {GlobalState} from 'types/store'; import {openModal} from './modals'; diff --git a/webapp/channels/src/actions/views/posts.js b/webapp/channels/src/actions/views/posts.js index 209e509deca..4c35f180e5e 100644 --- a/webapp/channels/src/actions/views/posts.js +++ b/webapp/channels/src/actions/views/posts.js @@ -1,22 +1,22 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import * as PostActions from 'mattermost-redux/actions/posts'; - -import {Permissions} from 'mattermost-redux/constants'; import {logError} from 'mattermost-redux/actions/errors'; -import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users'; -import {haveIChannelPermission, haveICurrentChannelPermission} from 'mattermost-redux/selectors/entities/roles'; +import * as PostActions from 'mattermost-redux/actions/posts'; +import {Permissions} from 'mattermost-redux/constants'; import {getLicense} from 'mattermost-redux/selectors/entities/general'; -import {isCustomGroupsEnabled} from 'mattermost-redux/selectors/entities/preferences'; import {getAssociatedGroupsForReferenceByMention} from 'mattermost-redux/selectors/entities/groups'; +import {isCustomGroupsEnabled} from 'mattermost-redux/selectors/entities/preferences'; +import {haveIChannelPermission, haveICurrentChannelPermission} from 'mattermost-redux/selectors/entities/roles'; import {getCurrentTeam} from 'mattermost-redux/selectors/entities/teams'; +import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users'; -import {getTimestamp} from 'utils/utils'; import {getPermalinkURL} from 'selectors/urls'; -import {getSiteURL} from 'utils/url'; -import {containsAtChannel, groupsMentionedInText} from 'utils/post_utils'; + import {ActionTypes, AnnouncementBarTypes} from 'utils/constants'; +import {containsAtChannel, groupsMentionedInText} from 'utils/post_utils'; +import {getSiteURL} from 'utils/url'; +import {getTimestamp} from 'utils/utils'; import {runMessageWillBePostedHooks} from '../hooks'; diff --git a/webapp/channels/src/actions/views/profile_popover.ts b/webapp/channels/src/actions/views/profile_popover.ts index 0aed9fd27b6..f6debb6aafe 100644 --- a/webapp/channels/src/actions/views/profile_popover.ts +++ b/webapp/channels/src/actions/views/profile_popover.ts @@ -1,10 +1,9 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import {getTeamMember} from 'mattermost-redux/actions/teams'; import {getChannelMember} from 'mattermost-redux/actions/channels'; - -import {DispatchFunc} from 'mattermost-redux/types/actions'; +import {getTeamMember} from 'mattermost-redux/actions/teams'; +import type {DispatchFunc} from 'mattermost-redux/types/actions'; export function getMembershipForEntities(teamId: string, userId: string, channelId?: string) { return (dispatch: DispatchFunc) => { diff --git a/webapp/channels/src/actions/views/rhs.test.ts b/webapp/channels/src/actions/views/rhs.test.ts index def9695af3a..92e1b374aa4 100644 --- a/webapp/channels/src/actions/views/rhs.test.ts +++ b/webapp/channels/src/actions/views/rhs.test.ts @@ -3,16 +3,18 @@ import {cloneDeep, set} from 'lodash'; import {batchActions} from 'redux-batched-actions'; -import {MockStoreEnhanced} from 'redux-mock-store'; +import type {MockStoreEnhanced} from 'redux-mock-store'; +import type {Post} from '@mattermost/types/posts'; +import type {UserProfile} from '@mattermost/types/users'; +import type {IDMappedObjects} from '@mattermost/types/utilities'; + +import {SearchTypes} from 'mattermost-redux/action_types'; import * as PostActions from 'mattermost-redux/actions/posts'; import * as SearchActions from 'mattermost-redux/actions/search'; -import {SearchTypes} from 'mattermost-redux/action_types'; -import {DispatchFunc} from 'mattermost-redux/types/actions'; -import {Post} from '@mattermost/types/posts'; -import {UserProfile} from '@mattermost/types/users'; -import {IDMappedObjects} from '@mattermost/types/utilities'; +import type {DispatchFunc} from 'mattermost-redux/types/actions'; +import {trackEvent} from 'actions/telemetry_actions.jsx'; import { updateRhsState, selectPostFromRightHandSideSearch, @@ -38,15 +40,15 @@ import { showChannelMembers, openShowEditHistory, } from 'actions/views/rhs'; -import {trackEvent} from 'actions/telemetry_actions.jsx'; + import mockStore from 'tests/test_store'; import {ActionTypes, RHSStates, Constants} from 'utils/constants'; import {TestHelper} from 'utils/test_helper'; import {getBrowserUtcOffset} from 'utils/timezone'; -import {GlobalState} from 'types/store'; -import {ViewsState} from 'types/store/views'; -import {RhsState} from 'types/store/rhs'; +import type {GlobalState} from 'types/store'; +import type {RhsState} from 'types/store/rhs'; +import type {ViewsState} from 'types/store/views'; const currentChannelId = '123'; const currentTeamId = '321'; @@ -67,7 +69,7 @@ global.Date.now = jest.fn(() => POST_CREATED_TIME); jest.mock('mattermost-redux/actions/posts', () => ({ getPostThread: (...args: any) => ({type: 'MOCK_GET_POST_THREAD', args}), - getProfilesAndStatusesForPosts: (...args: any) => ({type: 'MOCK_GET_PROFILES_AND_STATUSES_FOR_POSTS', args}), + getMentionsAndStatusesForPosts: (...args: any) => ({type: 'MOCK_GET_MENTIONS_AND_STATUSES_FOR_POSTS', args}), })); jest.mock('mattermost-redux/actions/search', () => ({ diff --git a/webapp/channels/src/actions/views/rhs.ts b/webapp/channels/src/actions/views/rhs.ts index 908d08a2be6..e95e86400c3 100644 --- a/webapp/channels/src/actions/views/rhs.ts +++ b/webapp/channels/src/actions/views/rhs.ts @@ -2,10 +2,15 @@ // See LICENSE.txt for license information. import debounce from 'lodash/debounce'; -import {AnyAction} from 'redux'; +import type {AnyAction} from 'redux'; import {batchActions} from 'redux-batched-actions'; +import type {Post} from '@mattermost/types/posts'; + import {SearchTypes} from 'mattermost-redux/action_types'; +import {getChannel} from 'mattermost-redux/actions/channels'; +import * as PostActions from 'mattermost-redux/actions/posts'; +import {getPostsByIds, getPost as fetchPost} from 'mattermost-redux/actions/posts'; import { clearSearch, getFlaggedPosts, @@ -13,30 +18,28 @@ import { searchPostsWithParams, searchFilesWithParams, } from 'mattermost-redux/actions/search'; -import * as PostActions from 'mattermost-redux/actions/posts'; -import {getCurrentUserMentionKeys} from 'mattermost-redux/selectors/entities/users'; -import {getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams'; -import {getConfig} from 'mattermost-redux/selectors/entities/general'; import {getCurrentChannelId, getCurrentChannelNameForSearchShortcut, getChannel as getChannelSelector} from 'mattermost-redux/selectors/entities/channels'; +import {getConfig} from 'mattermost-redux/selectors/entities/general'; import {getPost} from 'mattermost-redux/selectors/entities/posts'; +import {getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams'; import {getCurrentTimezone} from 'mattermost-redux/selectors/entities/timezone'; -import {Action, ActionResult, DispatchFunc, GenericAction, GetStateFunc} from 'mattermost-redux/types/actions'; -import {Post} from '@mattermost/types/posts'; +import {getCurrentUserMentionKeys} from 'mattermost-redux/selectors/entities/users'; +import type {Action, ActionResult, DispatchFunc, GenericAction, GetStateFunc} from 'mattermost-redux/types/actions'; import {trackEvent} from 'actions/telemetry_actions.jsx'; import {getSearchTerms, getRhsState, getPluggableId, getFilesSearchExtFilter, getPreviousRhsState} from 'selectors/rhs'; -import {ActionTypes, RHSStates, Constants} from 'utils/constants'; -import * as Utils from 'utils/utils'; -import {getBrowserUtcOffset, getUtcOffsetForTimeZone} from 'utils/timezone'; -import {RhsState} from 'types/store/rhs'; -import {GlobalState} from 'types/store'; -import {getPostsByIds, getPost as fetchPost} from 'mattermost-redux/actions/posts'; -import {getChannel} from 'mattermost-redux/actions/channels'; +import {SidebarSize} from 'components/resizable_sidebar/constants'; + +import {ActionTypes, RHSStates, Constants} from 'utils/constants'; +import {getBrowserUtcOffset, getUtcOffsetForTimeZone} from 'utils/timezone'; + +import type {GlobalState} from 'types/store'; +import type {RhsState} from 'types/store/rhs'; function selectPostFromRightHandSideSearchWithPreviousState(post: Post, previousRhsState?: RhsState) { return async (dispatch: DispatchFunc, getState: GetStateFunc) => { - const postRootId = Utils.getRootId(post); + const postRootId = post.root_id || post.id; await dispatch(PostActions.getPostThread(postRootId)); const state = getState() as GlobalState; @@ -138,6 +141,35 @@ export function updateSearchTerms(terms: string) { }; } +export function setRhsSize(rhsSize?: SidebarSize) { + let newSidebarSize = rhsSize; + if (!newSidebarSize) { + const width = window.innerWidth; + + switch (true) { + case width <= Constants.SMALL_SIDEBAR_BREAKPOINT: { + newSidebarSize = SidebarSize.SMALL; + break; + } + case width > Constants.SMALL_SIDEBAR_BREAKPOINT && width <= Constants.MEDIUM_SIDEBAR_BREAKPOINT: { + newSidebarSize = SidebarSize.MEDIUM; + break; + } + case width > Constants.MEDIUM_SIDEBAR_BREAKPOINT && width <= Constants.LARGE_SIDEBAR_BREAKPOINT: { + newSidebarSize = SidebarSize.LARGE; + break; + } + default: { + newSidebarSize = SidebarSize.XLARGE; + } + } + } + return { + type: ActionTypes.SET_RHS_SIZE, + size: newSidebarSize, + }; +} + export function updateSearchTermsForShortcut() { return (dispatch: DispatchFunc, getState: GetStateFunc) => { const currentChannelName = getCurrentChannelNameForSearchShortcut(getState()); diff --git a/webapp/channels/src/actions/views/root.test.ts b/webapp/channels/src/actions/views/root.test.ts index f22af072e0f..54c2b93bd7c 100644 --- a/webapp/channels/src/actions/views/root.test.ts +++ b/webapp/channels/src/actions/views/root.test.ts @@ -1,11 +1,11 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import {ActionTypes} from 'utils/constants'; import * as Actions from 'actions/views/root'; import * as i18nSelectors from 'selectors/i18n'; import mockStore from 'tests/test_store'; +import {ActionTypes} from 'utils/constants'; jest.mock('mattermost-redux/actions/general', () => { const original = jest.requireActual('mattermost-redux/actions/general'); diff --git a/webapp/channels/src/actions/views/root.ts b/webapp/channels/src/actions/views/root.ts index 6d3c9ac738a..5f863aaf98c 100644 --- a/webapp/channels/src/actions/views/root.ts +++ b/webapp/channels/src/actions/views/root.ts @@ -1,16 +1,18 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import {Client4} from 'mattermost-redux/client'; import {getClientConfig, getLicenseConfig} from 'mattermost-redux/actions/general'; import {loadMe, loadMeREST} from 'mattermost-redux/actions/users'; -import {DispatchFunc, GetStateFunc} from 'mattermost-redux/types/actions'; -import {GlobalState} from 'types/store'; +import {Client4} from 'mattermost-redux/client'; +import type {DispatchFunc, GetStateFunc} from 'mattermost-redux/types/actions'; import {getCurrentLocale, getTranslations} from 'selectors/i18n'; -import {Translations} from 'types/store/i18n'; -import {ActionTypes} from 'utils/constants'; + import en from 'i18n/en.json'; +import {ActionTypes} from 'utils/constants'; + +import type {GlobalState} from 'types/store'; +import type {Translations} from 'types/store/i18n'; const pluginTranslationSources: Record = {}; diff --git a/webapp/channels/src/actions/views/textbox.js b/webapp/channels/src/actions/views/textbox.js index 07b9254818c..49ac36a0f6b 100644 --- a/webapp/channels/src/actions/views/textbox.js +++ b/webapp/channels/src/actions/views/textbox.js @@ -1,7 +1,7 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import {ActionTypes} from '../../utils/constants'; +import {ActionTypes} from 'utils/constants'; export function setShowPreviewOnCreateComment(showPreview) { return { diff --git a/webapp/channels/src/actions/websocket_actions.jsx b/webapp/channels/src/actions/websocket_actions.jsx index 79388b0a497..5cf8740a21f 100644 --- a/webapp/channels/src/actions/websocket_actions.jsx +++ b/webapp/channels/src/actions/websocket_actions.jsx @@ -21,21 +21,32 @@ import { CloudTypes, HostedCustomerTypes, } from 'mattermost-redux/action_types'; -import {General, Permissions} from 'mattermost-redux/constants'; +import {getStandardAnalytics} from 'mattermost-redux/actions/admin'; +import {fetchAppBindings, fetchRHSAppsBindings} from 'mattermost-redux/actions/apps'; import {addChannelToInitialCategory, fetchMyCategories, receivedCategoryOrder} from 'mattermost-redux/actions/channel_categories'; import { getChannelAndMyMember, getMyChannelMember, getChannelStats, - viewChannel, - markChannelAsRead, + markMultipleChannelsAsRead, getChannelMemberCountsByGroup, } from 'mattermost-redux/actions/channels'; import {getCloudSubscription} from 'mattermost-redux/actions/cloud'; +import {clearErrors, logError} from 'mattermost-redux/actions/errors'; +import {setServerVersion, getClientConfig} from 'mattermost-redux/actions/general'; +import {getGroup as fetchGroup} from 'mattermost-redux/actions/groups'; +import { + getCustomEmojiForReaction, + getPosts, + getPostThread, + getMentionsAndStatusesForPosts, + getThreadsForPosts, + postDeleted, + receivedNewPost, + receivedPost, +} from 'mattermost-redux/actions/posts'; import {loadRolesIfNeeded} from 'mattermost-redux/actions/roles'; - -import {isCollapsedThreadsEnabled} from 'mattermost-redux/selectors/entities/preferences'; -import {getNewestThreadInTeam, getThread, getThreads} from 'mattermost-redux/selectors/entities/threads'; +import * as TeamActions from 'mattermost-redux/actions/teams'; import { getThread as fetchThread, getCountsAndThreadsSince, @@ -47,33 +58,14 @@ import { updateThreadRead, decrementThreadCounts, } from 'mattermost-redux/actions/threads'; - -import {setServerVersion, getClientConfig} from 'mattermost-redux/actions/general'; -import { - getCustomEmojiForReaction, - getPosts, - getPostThread, - getProfilesAndStatusesForPosts, - getThreadsForPosts, - postDeleted, - receivedNewPost, - receivedPost, -} from 'mattermost-redux/actions/posts'; -import {clearErrors, logError} from 'mattermost-redux/actions/errors'; - -import * as TeamActions from 'mattermost-redux/actions/teams'; import { checkForModifiedUsers, getUser as loadUser, } from 'mattermost-redux/actions/users'; import {removeNotVisibleUsers} from 'mattermost-redux/actions/websocket'; -import {setGlobalItem} from 'actions/storage'; -import {setGlobalDraft, transformServerDraft} from 'actions/views/drafts'; - import {Client4} from 'mattermost-redux/client'; -import {getCurrentUser, getCurrentUserId, getUser, getIsManualStatusForUserId, isCurrentUserSystemAdmin} from 'mattermost-redux/selectors/entities/users'; -import {getMyTeams, getCurrentRelativeTeamUrl, getCurrentTeamId, getCurrentTeamUrl, getTeam} from 'mattermost-redux/selectors/entities/teams'; -import {getConfig, getLicense} from 'mattermost-redux/selectors/entities/general'; +import {General, Permissions} from 'mattermost-redux/constants'; +import {appsFeatureFlagEnabled} from 'mattermost-redux/selectors/entities/apps'; import { getChannel, getChannelMembersInChannels, @@ -82,41 +74,52 @@ import { getCurrentChannelId, getRedirectChannelNameForTeam, } from 'mattermost-redux/selectors/entities/channels'; +import {getConfig, getLicense} from 'mattermost-redux/selectors/entities/general'; +import {getGroup} from 'mattermost-redux/selectors/entities/groups'; import {getPost, getMostRecentPostIdInChannel, getTeamIdFromPost} from 'mattermost-redux/selectors/entities/posts'; +import {isCollapsedThreadsEnabled} from 'mattermost-redux/selectors/entities/preferences'; import {haveISystemPermission, haveITeamPermission} from 'mattermost-redux/selectors/entities/roles'; -import {appsFeatureFlagEnabled} from 'mattermost-redux/selectors/entities/apps'; -import {getStandardAnalytics} from 'mattermost-redux/actions/admin'; - -import {fetchAppBindings, fetchRHSAppsBindings} from 'mattermost-redux/actions/apps'; - -import {getSelectedChannelId, getSelectedPost} from 'selectors/rhs'; -import {isThreadOpen, isThreadManuallyUnread} from 'selectors/views/threads'; - -import {openModal} from 'actions/views/modals'; -import {incrementWsErrorCount, resetWsErrorCount} from 'actions/views/system'; -import {closeRightHandSide} from 'actions/views/rhs'; -import {syncPostsInChannel} from 'actions/views/channel'; -import {updateThreadLastOpened} from 'actions/views/threads'; - -import {getHistory} from 'utils/browser_history'; -import {loadChannelsForCurrentUser} from 'actions/channel_actions'; -import {loadCustomEmojisIfNeeded} from 'actions/emoji_actions'; -import {redirectUserToDefaultTeam} from 'actions/global_actions'; -import {handleNewPost} from 'actions/post_actions'; -import * as StatusActions from 'actions/status_actions'; -import {loadProfilesForSidebar} from 'actions/user_actions'; -import {sendDesktopNotification} from 'actions/notification_actions.jsx'; -import store from 'stores/redux_store.jsx'; -import WebSocketClient from 'client/web_websocket_client.jsx'; -import {loadPlugin, loadPluginsIfNecessary, removePlugin} from 'plugins'; -import {ActionTypes, Constants, AnnouncementBarMessages, SocketEvents, UserStatuses, ModalIdentifiers, WarnMetricTypes} from 'utils/constants'; -import {getSiteURL} from 'utils/url'; +import { + getMyTeams, + getCurrentRelativeTeamUrl, + getCurrentTeamId, + getCurrentTeamUrl, + getTeam, + getRelativeTeamUrl, +} from 'mattermost-redux/selectors/entities/teams'; +import {getNewestThreadInTeam, getThread, getThreads} from 'mattermost-redux/selectors/entities/threads'; +import {getCurrentUser, getCurrentUserId, getUser, getIsManualStatusForUserId, isCurrentUserSystemAdmin} from 'mattermost-redux/selectors/entities/users'; import {isGuest} from 'mattermost-redux/utils/user_utils'; -import RemovedFromChannelModal from 'components/removed_from_channel_modal'; -import InteractiveDialog from 'components/interactive_dialog'; + +import {loadChannelsForCurrentUser} from 'actions/channel_actions'; import { getTeamsUsage, } from 'actions/cloud'; +import {loadCustomEmojisIfNeeded} from 'actions/emoji_actions'; +import {redirectUserToDefaultTeam} from 'actions/global_actions'; +import {sendDesktopNotification} from 'actions/notification_actions.jsx'; +import {handleNewPost} from 'actions/post_actions'; +import * as StatusActions from 'actions/status_actions'; +import {setGlobalItem} from 'actions/storage'; +import {loadProfilesForSidebar} from 'actions/user_actions'; +import {syncPostsInChannel} from 'actions/views/channel'; +import {setGlobalDraft, transformServerDraft} from 'actions/views/drafts'; +import {openModal} from 'actions/views/modals'; +import {closeRightHandSide} from 'actions/views/rhs'; +import {incrementWsErrorCount, resetWsErrorCount} from 'actions/views/system'; +import {updateThreadLastOpened} from 'actions/views/threads'; +import {getSelectedChannelId, getSelectedPost} from 'selectors/rhs'; +import {isThreadOpen, isThreadManuallyUnread} from 'selectors/views/threads'; +import store from 'stores/redux_store.jsx'; + +import InteractiveDialog from 'components/interactive_dialog'; +import RemovedFromChannelModal from 'components/removed_from_channel_modal'; + +import WebSocketClient from 'client/web_websocket_client.jsx'; +import {loadPlugin, loadPluginsIfNecessary, removePlugin} from 'plugins'; +import {getHistory} from 'utils/browser_history'; +import {ActionTypes, Constants, AnnouncementBarMessages, SocketEvents, UserStatuses, ModalIdentifiers, WarnMetricTypes} from 'utils/constants'; +import {getSiteURL} from 'utils/url'; const dispatch = store.dispatch; const getState = store.getState; @@ -456,8 +459,8 @@ export function handleEvent(msg) { handleAddEmoji(msg); break; - case SocketEvents.CHANNEL_VIEWED: - handleChannelViewedEvent(msg); + case SocketEvents.MULTIPLE_CHANNELS_VIEWED: + handleMultipleChannelsViewedEvent(msg); break; case SocketEvents.PLUGIN_ENABLED: @@ -619,11 +622,25 @@ export function handleChannelUpdatedEvent(msg) { return (doDispatch, doGetState) => { const channel = JSON.parse(msg.data.channel); - doDispatch({type: ChannelTypes.RECEIVED_CHANNEL, data: channel}); + const actions = [{type: ChannelTypes.RECEIVED_CHANNEL, data: channel}]; + // handling the case of GM converted to private channel. const state = doGetState(); + const existingChannel = getChannel(state, channel.id); + + // if the updated channel exists in store + if (existingChannel) { + // and it was a GM, converted to a private channel + if (existingChannel.type === General.GM_CHANNEL && channel.type === General.PRIVATE_CHANNEL) { + actions.push({type: ChannelTypes.GM_CONVERTED_TO_CHANNEL, data: channel}); + } + } + + doDispatch(batchActions(actions)); + if (channel.id === getCurrentChannelId(state)) { - getHistory().replace(`${getCurrentRelativeTeamUrl(state)}/channels/${channel.name}`); + // using channel's team_id to ensure we always redirect to current channel even if channel's team changes. + getHistory().replace(`${getRelativeTeamUrl(state, channel.team_id)}/channels/${channel.name}`); } }; } @@ -685,7 +702,7 @@ export function handleNewPostEvent(msg) { myDispatch(handleNewPost(post, msg)); - getProfilesAndStatusesForPosts([post], myDispatch, myGetState); + getMentionsAndStatusesForPosts([post], myDispatch, myGetState); // Since status updates aren't real time, assume another user is online if they have posted and: // 1. The user hasn't set their status manually to something that isn't online @@ -724,7 +741,7 @@ export function handleNewPostEvents(queue) { myDispatch(getThreadsForPosts(posts)); // And any other data needed for them - getProfilesAndStatusesForPosts(posts, myDispatch, myGetState); + getMentionsAndStatusesForPosts(posts, myDispatch, myGetState); }; } @@ -740,16 +757,7 @@ export function handlePostEditEvent(msg) { const crtEnabled = isCollapsedThreadsEnabled(getState()); dispatch(receivedPost(post, crtEnabled)); - getProfilesAndStatusesForPosts([post], dispatch, getState); - const currentChannelId = getCurrentChannelId(getState()); - - // Update channel state - if (currentChannelId === msg.broadcast.channel_id) { - dispatch(getChannelStats(currentChannelId)); - if (window.isActive) { - dispatch(viewChannel(currentChannelId)); - } - } + getMentionsAndStatusesForPosts([post], dispatch, getState); } async function handlePostDeleteEvent(msg) { @@ -1291,11 +1299,9 @@ function handleReactionRemovedEvent(msg) { }); } -function handleChannelViewedEvent(msg) { - // Useful for when multiple devices have the app open to different channels - if ((!window.isActive || getCurrentChannelId(getState()) !== msg.data.channel_id) && - getCurrentUserId(getState()) === msg.broadcast.user_id) { - dispatch(markChannelAsRead(msg.data.channel_id, '', false)); +function handleMultipleChannelsViewedEvent(msg) { + if (getCurrentUserId(getState()) === msg.broadcast.user_id) { + dispatch(markMultipleChannelsAsRead(msg.data.channel_times)); } } @@ -1367,20 +1373,32 @@ function handleGroupUpdatedEvent(msg) { ); } -function handleGroupAddedMemberEvent(msg) { - return (doDispatch, doGetState) => { +export function handleGroupAddedMemberEvent(msg) { + return async (doDispatch, doGetState) => { const state = doGetState(); const currentUserId = getCurrentUserId(state); - const data = JSON.parse(msg.data.group_member); + const groupInfo = JSON.parse(msg.data.group_member); - if (currentUserId === data.user_id) { - dispatch( - { - type: GroupTypes.ADD_MY_GROUP, - data, - id: data.group_id, - }, - ); + if (currentUserId === groupInfo.user_id) { + const group = getGroup(state, groupInfo.group_id); + if (group) { + dispatch( + { + type: GroupTypes.ADD_MY_GROUP, + id: groupInfo.group_id, + }, + ); + } else { + const {error} = await doDispatch(fetchGroup(groupInfo.group_id, true)); + if (!error) { + dispatch( + { + type: GroupTypes.ADD_MY_GROUP, + id: groupInfo.group_id, + }, + ); + } + } } }; } diff --git a/webapp/channels/src/actions/websocket_actions.test.jsx b/webapp/channels/src/actions/websocket_actions.test.jsx index f97cc74eed0..8b50d69d250 100644 --- a/webapp/channels/src/actions/websocket_actions.test.jsx +++ b/webapp/channels/src/actions/websocket_actions.test.jsx @@ -1,27 +1,25 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. +import {UserTypes, CloudTypes} from 'mattermost-redux/action_types'; +import {getGroup} from 'mattermost-redux/actions/groups'; import { - getProfilesAndStatusesForPosts, + getMentionsAndStatusesForPosts, getThreadsForPosts, receivedNewPost, } from 'mattermost-redux/actions/posts'; -import {ChannelTypes, UserTypes, CloudTypes} from 'mattermost-redux/action_types'; import {getUser} from 'mattermost-redux/actions/users'; import {handleNewPost} from 'actions/post_actions'; -import {closeRightHandSide} from 'actions/views/rhs'; import {syncPostsInChannel} from 'actions/views/channel'; - +import {closeRightHandSide} from 'actions/views/rhs'; import store from 'stores/redux_store.jsx'; +import mergeObjects from 'packages/mattermost-redux/test/merge_objects'; import configureStore from 'tests/test_store'; - import {getHistory} from 'utils/browser_history'; import Constants, {SocketEvents, UserStatuses, ActionTypes} from 'utils/constants'; -import mergeObjects from 'packages/mattermost-redux/test/merge_objects'; - import { handleChannelUpdatedEvent, handleEvent, @@ -37,12 +35,18 @@ import { handleAppsPluginEnabled, handleAppsPluginDisabled, handleCloudSubscriptionChanged, + handleGroupAddedMemberEvent, } from './websocket_actions'; jest.mock('mattermost-redux/actions/posts', () => ({ ...jest.requireActual('mattermost-redux/actions/posts'), getThreadsForPosts: jest.fn(() => ({type: 'GET_THREADS_FOR_POSTS'})), - getProfilesAndStatusesForPosts: jest.fn(), + getMentionsAndStatusesForPosts: jest.fn(), +})); + +jest.mock('mattermost-redux/actions/groups', () => ({ + ...jest.requireActual('mattermost-redux/actions/groups'), + getGroup: jest.fn(() => ({type: 'RECEIVED_GROUP'})), })); jest.mock('mattermost-redux/actions/users', () => ({ @@ -105,6 +109,20 @@ let mockState = { PluginsEnabled: 'true', }, }, + groups: { + syncables: {}, + groups: { + 'group-1': { + id: 'group-1', + name: 'group1', + display_name: 'Group 1', + member_count: 1, + allow_reference: true, + }, + }, + stats: {}, + myGroups: {}, + }, channels: { currentChannelId: 'otherChannel', channels: { @@ -202,6 +220,42 @@ describe('handlePostEditEvent', () => { }); }); +describe('handleGroupAddedMemberEvent', () => { + test('add to group in state', async () => { + const testStore = configureStore(mockState); + const msg = { + data: { + group_member: '{"group_id":"group-1","user_id":"currentUserId","create_at":1691178673417,"delete_at":0}', + }, + broadcast: { + user_id: 'currentUserId', + }, + }; + + testStore.dispatch(handleGroupAddedMemberEvent(msg)); + expect(store.dispatch).toHaveBeenCalledWith({ + type: 'ADD_MY_GROUP', + id: 'group-1', + }); + }); + + test('add to group not in state', async () => { + const testStore = configureStore(mockState); + const msg = { + data: { + group_member: '{"group_id":"group-2","user_id":"currentUserId","create_at":1691178673417,"delete_at":0}', + }, + broadcast: { + user_id: 'currentUserId', + }, + }; + + testStore.dispatch(handleGroupAddedMemberEvent(msg)); + expect(getGroup).toHaveBeenCalled(); + expect(testStore.getActions()).toEqual([{type: 'RECEIVED_GROUP'}]); + }); +}); + describe('handlePostUnreadEvent', () => { test('post marked as unred', async () => { const msgData = {last_viewed_at: 123, msg_count: 40, mention_count: 1}; @@ -434,7 +488,7 @@ describe('handleNewPostEvent', () => { }; testStore.dispatch(handleNewPostEvent(msg)); - expect(getProfilesAndStatusesForPosts).toHaveBeenCalledWith([post], expect.anything(), expect.anything()); + expect(getMentionsAndStatusesForPosts).toHaveBeenCalledWith([post], expect.anything(), expect.anything()); expect(handleNewPost).toHaveBeenCalledWith(post, msg); }); @@ -566,7 +620,7 @@ describe('handleNewPostEvents', () => { }, ]); expect(getThreadsForPosts).toHaveBeenCalledWith(posts); - expect(getProfilesAndStatusesForPosts).toHaveBeenCalledWith(posts, expect.anything(), expect.anything()); + expect(getMentionsAndStatusesForPosts).toHaveBeenCalledWith(posts, expect.anything(), expect.anything()); }); }); @@ -582,6 +636,11 @@ describe('handleChannelUpdatedEvent', () => { entities: { channels: { currentChannelId: 'channel', + channels: { + channel: { + id: 'channel', + }, + }, }, teams: { currentTeamId: 'team', @@ -595,20 +654,72 @@ describe('handleChannelUpdatedEvent', () => { test('when a channel is updated', () => { const testStore = configureStore(initialState); - const channel = {id: 'channel'}; + const channel = { + id: 'channel', + team_id: 'team', + }; const msg = {data: {channel: JSON.stringify(channel)}}; testStore.dispatch(handleChannelUpdatedEvent(msg)); + expect(testStore.getActions()).toEqual([{ + type: 'BATCHING_REDUCER.BATCH', + meta: {batch: true}, + payload: [ + { + type: 'RECEIVED_CHANNEL', + data: { + id: 'channel', + team_id: 'team', + }, + }, + ], + }]); + }); - expect(testStore.getActions()).toEqual([ - {type: ChannelTypes.RECEIVED_CHANNEL, data: channel}, - ]); + test('when GM is converted to private channel', () => { + const state = initialState; + state.entities.channels.channels.channel.type = Constants.GM_CHANNEL; + + const testStore = configureStore(state); + const channel = { + id: 'channel', + team_id: 'team', + type: Constants.PRIVATE_CHANNEL, + }; + + const msg = {data: {channel: JSON.stringify(channel)}}; + testStore.dispatch(handleChannelUpdatedEvent(msg)); + expect(testStore.getActions()).toEqual([{ + type: 'BATCHING_REDUCER.BATCH', + meta: {batch: true}, + payload: [ + { + type: 'RECEIVED_CHANNEL', + data: { + id: 'channel', + team_id: 'team', + type: 'P', + }, + }, + { + type: 'GM_CONVERTED_TO_CHANNEL', + data: { + id: 'channel', + team_id: 'team', + type: 'P', + }, + }, + ], + }]); }); test('should not change URL when current channel is updated', () => { const testStore = configureStore(initialState); - const channel = {id: 'channel'}; + const channel = { + id: 'channel', + team_id: 'team', + }; const msg = {data: {channel: JSON.stringify(channel)}}; testStore.dispatch(handleChannelUpdatedEvent(msg)); diff --git a/webapp/channels/src/components/__snapshots__/autosize_textarea.test.tsx.snap b/webapp/channels/src/components/__snapshots__/autosize_textarea.test.tsx.snap index 3bda56384f9..5b4e3eae43e 100644 --- a/webapp/channels/src/components/__snapshots__/autosize_textarea.test.tsx.snap +++ b/webapp/channels/src/components/__snapshots__/autosize_textarea.test.tsx.snap @@ -35,24 +35,15 @@ exports[`components/AutosizeTextarea should match snapshot, init 1`] = ` } } > -